@pixerate/editor 0.39.0 → 0.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -39,10 +39,20 @@ import {
39
39
  trimSegments,
40
40
  windingNumber
41
41
  } from "../chunk-3LHWTGHW.js";
42
+ import {
43
+ mixColorsOklab
44
+ } from "../chunk-ZRJMJIQU.js";
45
+ import {
46
+ evaluateEasing
47
+ } from "../chunk-UB2OY7FD.js";
48
+ import {
49
+ hexToRgba
50
+ } from "../chunk-TGD52FEZ.js";
42
51
 
43
52
  // src/vector/types.ts
44
53
  var VECTOR_CORNER_KINDS = ["round", "inverted", "chamfer"];
45
54
  var VECTOR_ARROWHEADS = ["triangle", "arrow", "open", "circle", "square", "bar"];
55
+ var VECTOR_BLEND_SPACINGS = ["steps", "distance", "smoothColor"];
46
56
  var VECTOR_TOOLS = [
47
57
  "select",
48
58
  "direct",
@@ -58,7 +68,8 @@ var VECTOR_TOOLS = [
58
68
  "eraser",
59
69
  "width",
60
70
  "shapeBuilder",
61
- "paintBucket"
71
+ "paintBucket",
72
+ "blend"
62
73
  ];
63
74
  var VECTOR_TRANSFORM_HANDLES = ["nw", "n", "ne", "e", "se", "s", "sw", "w", "rotate"];
64
75
  var VECTOR_PATHFINDER_OPERATIONS = ["divide", "trim", "merge", "crop", "outline", "minusBack"];
@@ -829,7 +840,11 @@ var capExtent = (style, width) => style.strokeLinecap === "round" || style.strok
829
840
  function arrowheadWeight(style, end) {
830
841
  const scale = end === "start" ? style.startArrowScale : style.endArrowScale;
831
842
  const s = typeof scale === "number" && scale > 0 && Number.isFinite(scale) ? scale : 1;
832
- return Math.max(0.25, (style.strokeWidth ?? 1) * s);
843
+ const width = style.strokeWidth ?? 1;
844
+ const profile = strokeWidthProfileOf(style);
845
+ const at2 = profile ? widthProfileAt(profile, end === "start" ? 0 : 1) : null;
846
+ const here = at2 ? Math.max(width, width * (at2.left + at2.right) / 2) : width;
847
+ return Math.max(0.25, here * s);
833
848
  }
834
849
  function arrowheadInset(kind, hw, cap) {
835
850
  const l = 4 * hw;
@@ -1323,9 +1338,25 @@ function decomposeRigid(m) {
1323
1338
  residual: [cos * a + sin * b, -sin * a + cos * b, cos * c + sin * d, -sin * c + cos * d, 0, 0]
1324
1339
  };
1325
1340
  }
1341
+ function mapAnchor(a, m) {
1342
+ const p = applyMatrix(m, a);
1343
+ const next = { x: p.x, y: p.y };
1344
+ if (a.in) next.in = applyMatrix(m, a.in);
1345
+ if (a.out) next.out = applyMatrix(m, a.out);
1346
+ if (a.type) next.type = a.type;
1347
+ if (a.corner) next.corner = { ...a.corner };
1348
+ return next;
1349
+ }
1326
1350
  function bakeIntoGeometry(node, m) {
1327
1351
  if (isIdentityMatrix(m)) return node;
1328
- if (node.type === "group") return { ...node, children: node.children.map((c) => transformVectorNode(c, m)) };
1352
+ if (node.type === "group") {
1353
+ const group = node;
1354
+ const children = group.children.map((c) => transformVectorNode(c, m));
1355
+ const spine = group.blend?.spine;
1356
+ if (!spine) return { ...group, children };
1357
+ const moved = { closed: spine.closed, anchors: spine.anchors.map((a) => mapAnchor(a, m)) };
1358
+ return { ...group, children, blend: { ...group.blend, spine: moved } };
1359
+ }
1329
1360
  const path = node;
1330
1361
  const mirrored = m[0] * m[3] - m[1] * m[2] < 0 && path.strokeWidthProfile?.length;
1331
1362
  return {
@@ -1333,15 +1364,7 @@ function bakeIntoGeometry(node, m) {
1333
1364
  ...mirrored ? { strokeWidthProfile: mirrorWidthProfile(path.strokeWidthProfile) } : {},
1334
1365
  subpaths: path.subpaths.map((sp) => ({
1335
1366
  closed: sp.closed,
1336
- anchors: sp.anchors.map((a) => {
1337
- const p = applyMatrix(m, a);
1338
- const next = { x: p.x, y: p.y };
1339
- if (a.in) next.in = applyMatrix(m, a.in);
1340
- if (a.out) next.out = applyMatrix(m, a.out);
1341
- if (a.type) next.type = a.type;
1342
- if (a.corner) next.corner = { ...a.corner };
1343
- return next;
1344
- })
1367
+ anchors: sp.anchors.map((a) => mapAnchor(a, m))
1345
1368
  }))
1346
1369
  };
1347
1370
  }
@@ -1609,6 +1632,33 @@ function gradientMarkup(def) {
1609
1632
  const stops = def.stops.map((s) => `<stop offset="${s.offset}" stop-color="${escapeAttr(s.stopColor)}"${s.stopOpacity !== void 0 ? ` stop-opacity="${s.stopOpacity}"` : ""}/>`).join("");
1610
1633
  return `<${tag} id="${escapeAttr(def.id)}"${attrs}>${stops}</${tag}>`;
1611
1634
  }
1635
+ function blendFromAttribute(value, m) {
1636
+ if (!value) return null;
1637
+ let raw;
1638
+ try {
1639
+ raw = JSON.parse(value);
1640
+ } catch {
1641
+ return null;
1642
+ }
1643
+ if (!raw || typeof raw !== "object") return null;
1644
+ const r = raw;
1645
+ const out = {};
1646
+ if (r.spacing === "steps" || r.spacing === "distance" || r.spacing === "smoothColor") out.spacing = r.spacing;
1647
+ if (typeof r.steps === "number" && Number.isFinite(r.steps)) out.steps = Math.min(1e3, Math.max(1, Math.round(r.steps)));
1648
+ if (typeof r.distance === "number" && r.distance > 0) out.distance = r.distance;
1649
+ if (typeof r.orientToSpine === "boolean") out.orientToSpine = r.orientToSpine;
1650
+ if (typeof r.linear === "boolean") out.linear = r.linear;
1651
+ if (r.starts && typeof r.starts === "object") {
1652
+ const starts = Object.fromEntries(Object.entries(r.starts).filter((e) => Number.isInteger(e[1]) && e[1] >= 0));
1653
+ if (Object.keys(starts).length) out.starts = starts;
1654
+ }
1655
+ const spine = r.spine;
1656
+ if (spine && Array.isArray(spine.anchors) && spine.anchors.every((a) => a && Number.isFinite(a.x) && Number.isFinite(a.y))) {
1657
+ 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 } } : {} })) };
1658
+ out.spine = m === IDENTITY_MATRIX ? sp : transformVectorNode({ id: "", type: "path", subpaths: [sp] }, m).subpaths[0];
1659
+ }
1660
+ return out;
1661
+ }
1612
1662
  function vectorDocumentToSvg(doc, options = {}) {
1613
1663
  const precision = options.precision ?? 3;
1614
1664
  const prefix = options.idPrefix ?? "vg-";
@@ -1622,6 +1672,8 @@ function vectorDocumentToSvg(doc, options = {}) {
1622
1672
  if (node.transform) out2 += ` transform="matrix(${node.transform.map((n) => formatNumber(n, 6)).join(" ")})"`;
1623
1673
  if (node.opacity !== void 0 && node.opacity < 1) out2 += ` opacity="${num(node.opacity)}"`;
1624
1674
  if (node.visible === false) out2 += ` display="none"`;
1675
+ if (node.blendStep) out2 += ` data-vector-blend-step=""`;
1676
+ if (node.type === "group" && node.blend) out2 += ` data-vector-blend="${escapeAttr(JSON.stringify(node.blend))}"`;
1625
1677
  return out2;
1626
1678
  };
1627
1679
  const emit = (node, depth) => {
@@ -1991,6 +2043,7 @@ function parseVectorSvg(svg, options = {}) {
1991
2043
  ...opacity !== void 0 && Number.isFinite(opacity) && opacity < 1 ? { opacity } : {},
1992
2044
  ...hidden ? { visible: false } : {}
1993
2045
  };
2046
+ if (child.attrs["data-vector-blend-step"] !== void 0) continue;
1994
2047
  if (child.name === "g" && child.attrs["data-vector-path"] !== void 0) {
1995
2048
  const node2 = mergedPath(child, s, m, meta);
1996
2049
  if (node2) out.push(node2);
@@ -2000,6 +2053,8 @@ function parseVectorSvg(svg, options = {}) {
2000
2053
  const children = visit(child, s, m);
2001
2054
  if (!children.length) continue;
2002
2055
  const group = { id: idFor(child), type: "group", ...meta, children };
2056
+ const blend = child.name === "g" ? blendFromAttribute(child.attrs["data-vector-blend"], m) : null;
2057
+ if (blend) group.blend = blend;
2003
2058
  out.push(group);
2004
2059
  continue;
2005
2060
  }
@@ -2025,6 +2080,910 @@ function parseVectorSvg(svg, options = {}) {
2025
2080
  return { width, height, nodes };
2026
2081
  }
2027
2082
 
2083
+ // src/vector/blend.ts
2084
+ var lerpN = (a, b, t) => a + (b - a) * t;
2085
+ var lerpP = (a, b, t) => [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t];
2086
+ var toP = (v2) => [v2.x, v2.y];
2087
+ var mapP = (m, p) => [m[0] * p[0] + m[2] * p[1] + m[4], m[1] * p[0] + m[3] * p[1] + m[5]];
2088
+ var curvesOf = (c) => Math.max(0, (c.pts.length - 1) / 3) | 0;
2089
+ function subpathToMorphContour(sp, m = IDENTITY_MATRIX) {
2090
+ const n = sp.anchors.length;
2091
+ if (!n) return null;
2092
+ const first = mapP(m, toP(sp.anchors[0]));
2093
+ const pts = [first];
2094
+ const count = n < 2 ? 0 : sp.closed ? n : n - 1;
2095
+ for (let i = 0; i < count; i++) {
2096
+ const a = sp.anchors[i];
2097
+ const b = sp.anchors[(i + 1) % n];
2098
+ const pa = toP(a);
2099
+ const pb = toP(b);
2100
+ 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);
2101
+ const c1 = straight ? lerpP(pa, pb, 1 / 3) : toP(a.out ?? a);
2102
+ const c2 = straight ? lerpP(pa, pb, 2 / 3) : toP(b.in ?? b);
2103
+ pts.push(mapP(m, c1), mapP(m, c2), i === count - 1 && sp.closed ? first : mapP(m, pb));
2104
+ }
2105
+ return { closed: sp.closed && count > 0, pts };
2106
+ }
2107
+ function isStraight(p0, p1, p2, p3) {
2108
+ const dx = p3[0] - p0[0];
2109
+ const dy = p3[1] - p0[1];
2110
+ const len2 = Math.hypot(dx, dy);
2111
+ const tol = Math.max(1e-7, len2 * 1e-7);
2112
+ 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;
2113
+ const off = (p) => Math.abs((p[0] - p0[0]) * dy - (p[1] - p0[1]) * dx) / len2;
2114
+ const along2 = (p) => ((p[0] - p0[0]) * dx + (p[1] - p0[1]) * dy) / (len2 * len2);
2115
+ const u1 = along2(p1);
2116
+ const u2 = along2(p2);
2117
+ return off(p1) < tol && off(p2) < tol && u1 >= -1e-9 && u1 <= 1 + 1e-9 && u2 >= -1e-9 && u2 <= 1 + 1e-9;
2118
+ }
2119
+ function morphContourToSubpath(c, m = IDENTITY_MATRIX, closed = c.closed) {
2120
+ const pts = isIdentityMatrix(m) ? c.pts : c.pts.map((p) => mapP(m, p));
2121
+ const curves = curvesOf(c);
2122
+ const dropLast = closed && c.closed && curves > 0;
2123
+ const count = dropLast ? curves : curves + 1;
2124
+ const anchors = [];
2125
+ for (let k = 0; k < count; k++) anchors.push({ x: pts[3 * k][0], y: pts[3 * k][1] });
2126
+ for (let k = 0; k < curves; k++) {
2127
+ const p0 = pts[3 * k];
2128
+ const p1 = pts[3 * k + 1];
2129
+ const p2 = pts[3 * k + 2];
2130
+ const p3 = pts[3 * k + 3];
2131
+ if (isStraight(p0, p1, p2, p3)) continue;
2132
+ anchors[k].out = { x: p1[0], y: p1[1] };
2133
+ anchors[(k + 1) % count].in = { x: p2[0], y: p2[1] };
2134
+ }
2135
+ for (const a of anchors) {
2136
+ const type = inferAnchorType(a);
2137
+ if (type !== "corner") a.type = type;
2138
+ }
2139
+ const isClosed = closed && curves > 0;
2140
+ return { anchors: dropCollinear(anchors, isClosed), closed: isClosed };
2141
+ }
2142
+ function dropCollinear(anchors, closed) {
2143
+ const out = anchors.slice();
2144
+ const min = closed ? 3 : 2;
2145
+ let i = closed ? 0 : 1;
2146
+ while (out.length > min && i < out.length - (closed ? 0 : 1)) {
2147
+ const n = out.length;
2148
+ const a = out[(i - 1 + n) % n];
2149
+ const b = out[i];
2150
+ const c = out[(i + 1) % n];
2151
+ const plain3 = !b.in && !b.out && !a.out && !c.in;
2152
+ const ux = b.x - a.x;
2153
+ const uy = b.y - a.y;
2154
+ const vx = c.x - b.x;
2155
+ const vy = c.y - b.y;
2156
+ const len2 = Math.hypot(ux, uy) + Math.hypot(vx, vy);
2157
+ if (plain3 && Math.abs(ux * vy - uy * vx) <= 1e-9 * Math.max(1, len2 * len2) && ux * vx + uy * vy >= 0) out.splice(i, 1);
2158
+ else i++;
2159
+ }
2160
+ return out;
2161
+ }
2162
+ var segOf = (c, k) => c.pts.slice(3 * k, 3 * k + 4);
2163
+ function refineMorphContour(c, segments) {
2164
+ const have = curvesOf(c);
2165
+ 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] };
2166
+ const identity = Array.from({ length: c.closed ? have : have + 1 }, (_, i) => i);
2167
+ if (have >= segments) return { contour: c, anchors: identity };
2168
+ const segs = Array.from({ length: have }, (_, k) => segOf(c, k));
2169
+ const lens = segs.map((s) => segmentLength(s));
2170
+ const sum = lens.reduce((a, b) => a + b, 0);
2171
+ const extra = segments - have;
2172
+ const want = lens.map((l) => sum > 1e-12 ? l / sum * extra : extra / have);
2173
+ const parts = want.map((w) => 1 + Math.floor(w));
2174
+ let left = segments - parts.reduce((a, b) => a + b, 0);
2175
+ const frac = want.map((w) => w - Math.floor(w));
2176
+ const order = parts.map((_, i) => i).sort((a, b) => Math.abs(frac[b] - frac[a]) > 1e-9 ? frac[b] - frac[a] : a - b);
2177
+ for (let i = 0; left > 0; i = (i + 1) % order.length, left--) parts[order[i]]++;
2178
+ const pts = [c.pts[0]];
2179
+ const anchors = [0];
2180
+ let at2 = 0;
2181
+ segs.forEach((seg, k) => {
2182
+ const pieces = parts[k];
2183
+ let rest = seg.slice();
2184
+ let t0 = 0;
2185
+ for (let j = 1; j < pieces; j++) {
2186
+ const t = segmentParamAtLength(seg, lens[k] * j / pieces, lens[k]);
2187
+ const u = Math.min(1, Math.max(0, (t - t0) / (1 - t0 || 1)));
2188
+ const [l, r] = splitSegment(rest, u);
2189
+ pts.push(l[1], l[2], l[3]);
2190
+ rest = r;
2191
+ t0 = t;
2192
+ }
2193
+ pts.push(rest[1], rest[2], k === have - 1 && c.closed ? c.pts[0] : rest[3]);
2194
+ at2 += pieces;
2195
+ if (k < have - 1 || !c.closed) anchors.push(at2);
2196
+ });
2197
+ return { contour: { closed: c.closed, pts }, anchors };
2198
+ }
2199
+ function reverseMorphContour(c) {
2200
+ return { closed: c.closed, pts: c.pts.slice().reverse() };
2201
+ }
2202
+ function turnMorphContour(c, s) {
2203
+ const n = curvesOf(c);
2204
+ if (!c.closed || n === 0) return c;
2205
+ const k = (s % n + n) % n;
2206
+ if (k === 0) return c;
2207
+ const body = c.pts.slice(1);
2208
+ return { closed: true, pts: [c.pts[3 * k], ...body.slice(3 * k), ...body.slice(0, 3 * k)] };
2209
+ }
2210
+ function anchorPoints(c) {
2211
+ const n = curvesOf(c);
2212
+ const count = c.closed ? n : n + 1;
2213
+ return Array.from({ length: Math.max(1, count) }, (_, k) => c.pts[3 * k]);
2214
+ }
2215
+ function polygonArea(pts) {
2216
+ let s = 0;
2217
+ for (let i = 0; i < pts.length; i++) {
2218
+ const a = pts[i];
2219
+ const b = pts[(i + 1) % pts.length];
2220
+ s += a[0] * b[1] - b[0] * a[1];
2221
+ }
2222
+ return s / 2;
2223
+ }
2224
+ function frame(v2) {
2225
+ const n = Math.max(1, v2.length);
2226
+ let cx = 0;
2227
+ let cy = 0;
2228
+ for (const p of v2) {
2229
+ cx += p[0];
2230
+ cy += p[1];
2231
+ }
2232
+ cx /= n;
2233
+ cy /= n;
2234
+ let s = 0;
2235
+ for (const p of v2) s += (p[0] - cx) ** 2 + (p[1] - cy) ** 2;
2236
+ return { c: [cx, cy], r: Math.max(1e-9, Math.sqrt(s / n)) };
2237
+ }
2238
+ var RBINS = 5;
2239
+ var ABINS = 12;
2240
+ var TURN_WEIGHT = 0.1;
2241
+ function shapeContexts(v2, closed) {
2242
+ const { r } = frame(v2);
2243
+ const n = v2.length;
2244
+ return v2.map((p, i) => {
2245
+ const prev = v2[closed ? (i - 1 + n) % n : Math.max(0, i - 1)];
2246
+ const next = v2[closed ? (i + 1) % n : Math.min(n - 1, i + 1)];
2247
+ const dir = Math.atan2(next[1] - prev[1], next[0] - prev[0]);
2248
+ const h = new Float64Array(RBINS * ABINS);
2249
+ let total = 0;
2250
+ for (let j = 0; j < n; j++) {
2251
+ if (i === j) continue;
2252
+ const dx = v2[j][0] - p[0];
2253
+ const dy = v2[j][1] - p[1];
2254
+ const d = Math.hypot(dx, dy) / r;
2255
+ const lr = (Math.log2(Math.max(d, 1e-6)) + 3) / 4 * RBINS;
2256
+ if (!(lr >= 0 && lr < RBINS)) continue;
2257
+ let ang = Math.atan2(dy, dx) - dir;
2258
+ ang = (ang % (2 * Math.PI) + 2 * Math.PI) % (2 * Math.PI);
2259
+ h[Math.floor(lr) * ABINS + Math.min(ABINS - 1, Math.floor(ang / (2 * Math.PI) * ABINS))] += 1;
2260
+ total++;
2261
+ }
2262
+ if (total > 0) for (let k = 0; k < h.length; k++) h[k] /= total;
2263
+ return h;
2264
+ });
2265
+ }
2266
+ function chi2(a, b) {
2267
+ let s = 0;
2268
+ for (let k = 0; k < a.length; k++) {
2269
+ const t = a[k] + b[k];
2270
+ if (t > 1e-12) s += (a[k] - b[k]) * (a[k] - b[k]) / t;
2271
+ }
2272
+ return s / 2;
2273
+ }
2274
+ function bestStart(fixed, free, useShapeContext) {
2275
+ const fa = anchorPoints(fixed);
2276
+ const ga = anchorPoints(free);
2277
+ const n = ga.length;
2278
+ const ff = frame(fa);
2279
+ const gf = frame(ga);
2280
+ const fn = fa.map((p) => [(p[0] - ff.c[0]) / ff.r, (p[1] - ff.c[1]) / ff.r]);
2281
+ const gn = ga.map((p) => [(p[0] - gf.c[0]) / gf.r, (p[1] - gf.c[1]) / gf.r]);
2282
+ const fsc = useShapeContext ? shapeContexts(fa, fixed.closed) : null;
2283
+ const gsc = useShapeContext ? shapeContexts(ga, free.closed) : null;
2284
+ const m = Math.max(1, fn.length);
2285
+ const cost = (s) => {
2286
+ let dot2 = 0;
2287
+ let cross2 = 0;
2288
+ let sc = 0;
2289
+ for (let i = 0; i < fn.length; i++) {
2290
+ const j = free.closed ? (i + s) % n : i;
2291
+ const p = fn[i];
2292
+ const q = gn[j];
2293
+ dot2 += p[0] * q[0] + p[1] * q[1];
2294
+ cross2 += p[0] * q[1] - p[1] * q[0];
2295
+ if (fsc && gsc) sc += chi2(fsc[i], gsc[j]);
2296
+ }
2297
+ const residual = Math.max(0, 1 - (dot2 * dot2 + cross2 * cross2) / (m * m));
2298
+ const turn = 1 - Math.cos(Math.atan2(cross2, dot2));
2299
+ return residual + TURN_WEIGHT * turn + sc / m;
2300
+ };
2301
+ if (!free.closed) return { start: 0, cost: cost(0) };
2302
+ const stride = n > 96 ? Math.ceil(n / 48) : 1;
2303
+ let best = { start: 0, cost: Infinity };
2304
+ for (let s = 0; s < n; s += stride) {
2305
+ const c = cost(s);
2306
+ if (c < best.cost - 1e-12) best = { start: s, cost: c };
2307
+ }
2308
+ if (stride > 1) {
2309
+ const s0 = best.start;
2310
+ for (let d = 1; d < stride; d++)
2311
+ for (const s of [(s0 + d) % n, (s0 - d + n) % n]) {
2312
+ const c = cost(s);
2313
+ if (c < best.cost - 1e-12) best = { start: s, cost: c };
2314
+ }
2315
+ }
2316
+ return best;
2317
+ }
2318
+ function placeIn(c, from, to) {
2319
+ const b = unionBounds([boundsOfPoints(c.pts)]);
2320
+ const p = b ? [b.x + b.width / 2, b.y + b.height / 2] : c.pts[0];
2321
+ if (!from || !to) return p;
2322
+ const fx = from.width > 0 ? (p[0] - from.x) / from.width : 0.5;
2323
+ const fy = from.height > 0 ? (p[1] - from.y) / from.height : 0.5;
2324
+ return [to.x + to.width * fx, to.y + to.height * fy];
2325
+ }
2326
+ function boundsOfPoints(pts) {
2327
+ if (!pts.length) return null;
2328
+ let x0 = Infinity;
2329
+ let y0 = Infinity;
2330
+ let x1 = -Infinity;
2331
+ let y1 = -Infinity;
2332
+ for (const p of pts) {
2333
+ x0 = Math.min(x0, p[0]);
2334
+ y0 = Math.min(y0, p[1]);
2335
+ x1 = Math.max(x1, p[0]);
2336
+ y1 = Math.max(y1, p[1]);
2337
+ }
2338
+ return { x: x0, y: y0, width: x1 - x0, height: y1 - y0 };
2339
+ }
2340
+ function correspondMorphContours(a, b, options = {}) {
2341
+ const minSeg = Math.max(1, Math.round(options.minSegments ?? 16));
2342
+ const maxSeg = Math.max(minSeg, Math.round(options.maxSegments ?? 400));
2343
+ const spacing = options.spacing && options.spacing > 0 ? options.spacing : null;
2344
+ const useSC = options.shapeContext !== false;
2345
+ const match = options.matchStart !== false;
2346
+ const ba = boundsOfPoints(a.flatMap((c) => c.pts));
2347
+ const bb = boundsOfPoints(b.flatMap((c) => c.pts));
2348
+ const contourLength = (c) => {
2349
+ let l = 0;
2350
+ for (let k = 0; k < curvesOf(c); k++) l += segmentLength(segOf(c, k));
2351
+ return l;
2352
+ };
2353
+ const pairs = [];
2354
+ const [sa, sb] = options.starts ?? [];
2355
+ for (let k = 0; k < Math.max(a.length, b.length); k++) {
2356
+ const ca = a[k];
2357
+ const cb = b[k];
2358
+ const explicitA = k === 0 && sa !== null && sa !== void 0 ? sa : null;
2359
+ const explicitB = k === 0 && sb !== null && sb !== void 0 ? sb : null;
2360
+ const counts = [ca ? curvesOf(ca) : 0, cb ? curvesOf(cb) : 0];
2361
+ const longest = Math.max(ca ? contourLength(ca) : 0, cb ? contourLength(cb) : 0);
2362
+ let n = Math.max(counts[0], counts[1], minSeg, spacing ? Math.ceil(longest / spacing) : 0);
2363
+ n = Math.max(counts[0], counts[1], Math.min(n, maxSeg));
2364
+ const point = (c, from, to) => {
2365
+ const p = placeIn(c, from, to);
2366
+ return { closed: c.closed, pts: Array.from({ length: 1 + 3 * n }, () => [p[0], p[1]]) };
2367
+ };
2368
+ if (ca && cb) {
2369
+ const ra = refineMorphContour(ca, n);
2370
+ const rb = refineMorphContour(cb, n);
2371
+ pairs.push({
2372
+ x: ra.contour,
2373
+ y: rb.contour,
2374
+ point: false,
2375
+ startX: explicitA === null ? null : ra.anchors[explicitA] ?? null,
2376
+ startY: explicitB === null ? null : explicitB === rb.anchors.length - 1 && !cb.closed ? -1 : rb.anchors[explicitB] ?? null
2377
+ });
2378
+ if (explicitA !== null && !ca.closed && explicitA === ra.anchors.length - 1 && explicitA > 0) pairs[pairs.length - 1].startX = -1;
2379
+ } else if (ca) pairs.push({ x: refineMorphContour(ca, n).contour, y: point(ca, ba, bb), point: true, startX: null, startY: null });
2380
+ else if (cb) pairs.push({ x: point(cb, bb, ba), y: refineMorphContour(cb, n).contour, point: true, startX: null, startY: null });
2381
+ }
2382
+ const startAt = (c, s) => {
2383
+ if (s === null) return c;
2384
+ if (s === -1) return reverseMorphContour(c);
2385
+ return c.closed ? turnMorphContour(c, s) : c;
2386
+ };
2387
+ const explicit = pairs.length > 0 && (pairs[0].startX !== null || pairs[0].startY !== null);
2388
+ const ref = pairs.find((p) => !p.point && p.x.closed && p.y.closed);
2389
+ 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;
2390
+ let best = null;
2391
+ for (const back of fixedBack === null ? [false, true] : [fixedBack]) {
2392
+ const xs = [];
2393
+ const ys = [];
2394
+ let cost = 0;
2395
+ pairs.forEach((p, k) => {
2396
+ let x = k === 0 ? startAt(p.x, p.startX) : p.x;
2397
+ let y = k === 0 ? startAt(p.y, p.startY) : p.y;
2398
+ if (back) y = reverseMorphContour(y);
2399
+ if (p.point) {
2400
+ xs.push(x);
2401
+ ys.push(y);
2402
+ return;
2403
+ }
2404
+ const pinned = k === 0 && explicit;
2405
+ const turnX = !y.closed && x.closed;
2406
+ if (match && !pinned && (y.closed || turnX)) {
2407
+ const r = turnX ? bestStart(y, x, useSC) : bestStart(x, y, useSC);
2408
+ if (turnX) x = turnMorphContour(x, r.start);
2409
+ else y = turnMorphContour(y, r.start);
2410
+ cost += r.cost;
2411
+ } else cost += bestStart(x, { ...y, closed: false }, useSC).cost;
2412
+ xs.push(x);
2413
+ ys.push(y);
2414
+ });
2415
+ if (!best || cost < best.cost - 1e-9) best = { xs, ys, cost, back };
2416
+ }
2417
+ return best ? { a: best.xs, b: best.ys, reversed: best.back } : { a: [], b: [], reversed: false };
2418
+ }
2419
+ function morphPathsCross(a, b) {
2420
+ let count = 0;
2421
+ const d = (p, q, r) => (q[0] - p[0]) * (r[1] - p[1]) - (q[1] - p[1]) * (r[0] - p[0]);
2422
+ for (let k = 0; k < Math.min(a.length, b.length); k++) {
2423
+ const pa = anchorPoints(a[k]);
2424
+ const pb = anchorPoints(b[k]);
2425
+ const n = Math.min(pa.length, pb.length);
2426
+ for (let i = 0; i < n; i++)
2427
+ for (let j = i + 1; j < n; j++) {
2428
+ const d1 = d(pa[j], pb[j], pa[i]);
2429
+ const d2 = d(pa[j], pb[j], pb[i]);
2430
+ const d3 = d(pa[i], pb[i], pa[j]);
2431
+ const d4 = d(pa[i], pb[i], pb[j]);
2432
+ if (d1 * d2 < 0 && d3 * d4 < 0) count++;
2433
+ }
2434
+ }
2435
+ return count;
2436
+ }
2437
+ function procrustes(a, b) {
2438
+ const pa = a.flatMap(anchorPoints);
2439
+ const pb = b.flatMap(anchorPoints);
2440
+ const ca = frame(pa).c;
2441
+ const cb = frame(pb).c;
2442
+ let ss = 0;
2443
+ let dot2 = 0;
2444
+ let cross2 = 0;
2445
+ for (let i = 0; i < Math.min(pa.length, pb.length); i++) {
2446
+ const p = [pa[i][0] - ca[0], pa[i][1] - ca[1]];
2447
+ const q = [pb[i][0] - cb[0], pb[i][1] - cb[1]];
2448
+ ss += p[0] * p[0] + p[1] * p[1];
2449
+ dot2 += p[0] * q[0] + p[1] * q[1];
2450
+ cross2 += p[0] * q[1] - p[1] * q[0];
2451
+ }
2452
+ return { ca, cb, theta: Math.atan2(cross2, dot2), s: ss > 1e-12 ? Math.hypot(dot2, cross2) / ss : 1 };
2453
+ }
2454
+ function interpolateMorphContours(a, b, u, options = {}) {
2455
+ const w = options.linear ? 0 : Math.min(1, Math.max(0, options.rigidity ?? 1));
2456
+ const sim = w > 0 ? options.similarity ?? procrustes(a, b) : null;
2457
+ const rot = (p, ang, k) => {
2458
+ const c = Math.cos(ang);
2459
+ const s = Math.sin(ang);
2460
+ return [k * (c * p[0] - s * p[1]), k * (s * p[0] + c * p[1])];
2461
+ };
2462
+ const thetaU = sim ? sim.theta * u : 0;
2463
+ const sU = sim ? Math.max(1e-9, sim.s) ** u : 1;
2464
+ const cU = sim ? lerpP(sim.ca, sim.cb, u) : [0, 0];
2465
+ return a.map((ca, k) => {
2466
+ const cb = b[k];
2467
+ const pts = ca.pts.map((p, i) => {
2468
+ const q = cb.pts[i] ?? p;
2469
+ const linear = lerpP(p, q, u);
2470
+ if (!sim) return linear;
2471
+ const d = [p[0] - sim.ca[0], p[1] - sim.ca[1]];
2472
+ const full = rot(d, sim.theta, sim.s);
2473
+ const resid = [q[0] - (full[0] + sim.cb[0]), q[1] - (full[1] + sim.cb[1])];
2474
+ const part = rot(d, thetaU, sU);
2475
+ const rigid = [part[0] + cU[0] + resid[0] * u, part[1] + cU[1] + resid[1] * u];
2476
+ return lerpP(linear, rigid, w);
2477
+ });
2478
+ return { closed: ca.closed && cb.closed, pts };
2479
+ });
2480
+ }
2481
+ function interpolateRigidMatrix(a, b, t) {
2482
+ const decompose = (m) => {
2483
+ const sx2 = Math.hypot(m[0], m[1]);
2484
+ const ang2 = sx2 > 1e-12 ? Math.atan2(m[1], m[0]) : 0;
2485
+ const c2 = Math.cos(ang2);
2486
+ const s2 = Math.sin(ang2);
2487
+ 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] };
2488
+ };
2489
+ const p = decompose(a);
2490
+ const q = decompose(b);
2491
+ let turn = q.ang - p.ang;
2492
+ turn = ((turn + Math.PI) % (2 * Math.PI) + 2 * Math.PI) % (2 * Math.PI) - Math.PI;
2493
+ const ang = p.ang + turn * t;
2494
+ const sx = lerpN(p.sx, q.sx, t);
2495
+ const sh = lerpN(p.sh, q.sh, t);
2496
+ const sy = lerpN(p.sy, q.sy, t);
2497
+ const c = Math.cos(ang);
2498
+ const s = Math.sin(ang);
2499
+ 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)];
2500
+ }
2501
+ function interpolateVectorColor(a, b, t) {
2502
+ if (t <= 0) return a;
2503
+ if (t >= 1) return b;
2504
+ return mixColorsOklab(a, b, t) ?? (t < 0.5 ? a : b);
2505
+ }
2506
+ function sortedStops(g) {
2507
+ return g.stops.slice().sort((x, y) => x.offset - y.offset);
2508
+ }
2509
+ function sampleGradient(stops, offset) {
2510
+ if (!stops.length) return { color: "transparent", opacity: 1 };
2511
+ const first = stops[0];
2512
+ const last = stops[stops.length - 1];
2513
+ if (offset <= first.offset) return { color: first.color, opacity: first.opacity ?? 1 };
2514
+ if (offset >= last.offset) return { color: last.color, opacity: last.opacity ?? 1 };
2515
+ let i = 0;
2516
+ while (i + 1 < stops.length && stops[i + 1].offset < offset) i++;
2517
+ const x = stops[i];
2518
+ const y = stops[i + 1];
2519
+ const u = y.offset - x.offset > 1e-12 ? (offset - x.offset) / (y.offset - x.offset) : 1;
2520
+ return { color: interpolateVectorColor(x.color, y.color, u), opacity: lerpN(x.opacity ?? 1, y.opacity ?? 1, u) };
2521
+ }
2522
+ function resampleGradientStops(g, offsets) {
2523
+ const stops = sortedStops(g);
2524
+ return offsets.map((offset) => {
2525
+ const s = sampleGradient(stops, offset);
2526
+ return s.opacity < 1 ? { offset, color: s.color, opacity: s.opacity } : { offset, color: s.color };
2527
+ });
2528
+ }
2529
+ function lerpAngle(a, b, t) {
2530
+ const d = ((b - a + 180) % 360 + 360) % 360 - 180;
2531
+ return a + d * t;
2532
+ }
2533
+ function lerpGradient(a, b, t) {
2534
+ if (a.type !== b.type) return null;
2535
+ let stopsA = sortedStops(a);
2536
+ let stopsB = sortedStops(b);
2537
+ if (stopsA.length !== stopsB.length) {
2538
+ const offs = [...stopsA, ...stopsB].map((s) => s.offset).filter(Number.isFinite).sort((x, y) => x - y);
2539
+ const common = offs.filter((o, i) => i === 0 || o - offs[i - 1] > 1e-4);
2540
+ stopsA = resampleGradientStops(a, common);
2541
+ stopsB = resampleGradientStops(b, common);
2542
+ }
2543
+ const stops = stopsA.map((x, i) => {
2544
+ const y = stopsB[i];
2545
+ const opacity = lerpN(x.opacity ?? 1, y.opacity ?? 1, t);
2546
+ const stop = { offset: lerpN(x.offset, y.offset, t), color: interpolateVectorColor(x.color, y.color, t) };
2547
+ if (opacity < 1) stop.opacity = opacity;
2548
+ return stop;
2549
+ });
2550
+ if (a.type === "linear" && b.type === "linear") return { type: "linear", angle: lerpAngle(a.angle ?? 90, b.angle ?? 90, t), stops };
2551
+ const ra = a;
2552
+ const rb = b;
2553
+ 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 };
2554
+ }
2555
+ function solidLike(color, like) {
2556
+ return { ...like, stops: like.stops.map((s) => color === null ? { offset: s.offset, color: s.color, opacity: 0 } : { offset: s.offset, color }) };
2557
+ }
2558
+ function interpolateVectorPaint(a, b, t) {
2559
+ if (t <= 0) return a;
2560
+ if (t >= 1) return b;
2561
+ const pa = hasPaint(a);
2562
+ const pb = hasPaint(b);
2563
+ if (!pa && !pb) return t < 0.5 ? a : b;
2564
+ if (typeof a === "string" && pa && typeof b === "string" && pb) return interpolateVectorColor(a, b, t);
2565
+ if (!pa && typeof b === "string") return interpolateVectorColor("transparent", b, t);
2566
+ if (!pb && typeof a === "string") return interpolateVectorColor(a, "transparent", t);
2567
+ const ga = pa && typeof a === "object" && a ? a : null;
2568
+ const gb = pb && typeof b === "object" && b ? b : null;
2569
+ if (ga && gb) return lerpGradient(ga, gb, t) ?? (t < 0.5 ? a : b);
2570
+ if (gb) return lerpGradient(solidLike(pa ? a : null, gb), gb, t) ?? b;
2571
+ if (ga) return lerpGradient(ga, solidLike(pb ? b : null, ga), t) ?? a;
2572
+ return t < 0.5 ? a : b;
2573
+ }
2574
+ function interpolateWidthProfiles(a, b, t) {
2575
+ if (!a?.length && !b?.length) return null;
2576
+ const at2 = [...a ?? [], ...b ?? [], { t: 0 }, { t: 1 }].map((p) => p.t).sort((x, y) => x - y);
2577
+ const ts = at2.filter((v2, i) => i === 0 || v2 - at2[i - 1] > 1e-6);
2578
+ return ts.map((pos) => {
2579
+ const x = widthProfileAt(a, pos);
2580
+ const y = widthProfileAt(b, pos);
2581
+ return { t: pos, left: lerpN(x.left, y.left, t), right: lerpN(x.right, y.right, t) };
2582
+ });
2583
+ }
2584
+ function lerpDashes(a, b, t) {
2585
+ const dashed = (d) => !!d?.length && d.some((v2) => v2 > 0);
2586
+ if (!dashed(a) && !dashed(b)) return t < 0.5 ? a : b;
2587
+ const even = (p) => p.length % 2 ? [...p, ...p] : p;
2588
+ const solid = (p) => {
2589
+ const e = even(p);
2590
+ const out = [];
2591
+ for (let i = 0; i < e.length; i += 2) out.push(e[i] + e[i + 1], 0);
2592
+ return out;
2593
+ };
2594
+ const x = dashed(a) ? even(a) : solid(b);
2595
+ const y = dashed(b) ? even(b) : solid(a);
2596
+ if (x.length !== y.length) return t < 0.5 ? a : b;
2597
+ return x.map((v2, i) => lerpN(v2, y[i], t));
2598
+ }
2599
+ var SWITCHED_KEYS = ["strokeLinecap", "strokeLinejoin", "fillRule", "strokeAlign", "startArrow", "endArrow", "arrowAlign"];
2600
+ function interpolateVectorStyle(a, b, t) {
2601
+ const near2 = t < 0.5 ? a : b;
2602
+ const out = {};
2603
+ const fill = interpolateVectorPaint(a.fill, b.fill, t);
2604
+ if (fill !== void 0) out.fill = fill;
2605
+ const stroke = interpolateVectorPaint(a.stroke, b.stroke, t);
2606
+ if (stroke !== void 0) out.stroke = stroke;
2607
+ if (a.strokeWidth !== void 0 || b.strokeWidth !== void 0) out.strokeWidth = lerpN(a.strokeWidth ?? 1, b.strokeWidth ?? 1, t);
2608
+ for (const key of SWITCHED_KEYS) if (near2[key] !== void 0 && near2[key] !== null) out[key] = near2[key];
2609
+ for (const key of ["startArrowScale", "endArrowScale"])
2610
+ if (a[key] !== void 0 || b[key] !== void 0) out[key] = lerpN(a[key] ?? 1, b[key] ?? 1, t);
2611
+ const dash = lerpDashes(a.strokeDasharray, b.strokeDasharray, t);
2612
+ if (dash?.length) out.strokeDasharray = dash;
2613
+ const profile = interpolateWidthProfiles(a.strokeWidthProfile, b.strokeWidthProfile, t);
2614
+ if (profile) out.strokeWidthProfile = profile;
2615
+ return out;
2616
+ }
2617
+ function reversedStyle(s) {
2618
+ const out = { ...s };
2619
+ out.startArrow = s.endArrow;
2620
+ out.endArrow = s.startArrow;
2621
+ out.startArrowScale = s.endArrowScale;
2622
+ out.endArrowScale = s.startArrowScale;
2623
+ if (s.strokeWidthProfile?.length) out.strokeWidthProfile = reverseWidthProfile(s.strokeWidthProfile);
2624
+ for (const k of ["startArrow", "endArrow", "startArrowScale", "endArrowScale"]) if (out[k] === void 0) delete out[k];
2625
+ return out;
2626
+ }
2627
+ var STYLE_KEYS2 = [
2628
+ "fill",
2629
+ "stroke",
2630
+ "strokeWidth",
2631
+ "strokeLinecap",
2632
+ "strokeLinejoin",
2633
+ "strokeDasharray",
2634
+ "fillRule",
2635
+ "strokeAlign",
2636
+ "startArrow",
2637
+ "endArrow",
2638
+ "arrowAlign",
2639
+ "startArrowScale",
2640
+ "endArrowScale",
2641
+ "strokeWidthProfile"
2642
+ ];
2643
+ function styleOf(n) {
2644
+ const out = {};
2645
+ for (const k of STYLE_KEYS2) if (n[k] !== void 0) out[k] = n[k];
2646
+ return out;
2647
+ }
2648
+ function contoursOf(node, m) {
2649
+ return expandVectorCorners(node.subpaths).map((sp) => subpathToMorphContour(sp, m)).filter((c) => !!c);
2650
+ }
2651
+ function preparePathMorph(a, b, options, ma = nodeMatrix(a), mb = nodeMatrix(b)) {
2652
+ const corr = correspondMorphContours(contoursOf(a, ma), contoursOf(b, mb), options);
2653
+ const sim = procrustes(corr.a, corr.b);
2654
+ const styleA = styleOf(a);
2655
+ const styleB = corr.reversed ? reversedStyle(styleOf(b)) : styleOf(b);
2656
+ const rigid = !isIdentityMatrix(ma) || !isIdentityMatrix(mb);
2657
+ const id = options.id ?? a.id;
2658
+ return (t) => {
2659
+ const contours = interpolateMorphContours(corr.a, corr.b, t, { linear: options.linear, rigidity: options.rigidity, similarity: sim });
2660
+ const m = rigid ? interpolateRigidMatrix(ma, mb, t) : IDENTITY_MATRIX;
2661
+ const back = rigid ? invertMatrix(m) : IDENTITY_MATRIX;
2662
+ const subpaths = contours.map((c, k) => {
2663
+ const closed = t <= 0 ? corr.a[k].closed : t >= 1 ? corr.b[k].closed : corr.a[k].closed && corr.b[k].closed;
2664
+ return morphContourToSubpath({ closed: corr.a[k].closed && corr.b[k].closed, pts: c.pts }, back, closed);
2665
+ });
2666
+ const node = { id, type: "path", subpaths, ...interpolateVectorStyle(styleA, styleB, t) };
2667
+ const opacity = lerpN(a.opacity ?? 1, b.opacity ?? 1, t);
2668
+ if (opacity < 1) node.opacity = opacity;
2669
+ if (rigid && !isIdentityMatrix(m)) node.transform = m;
2670
+ return node;
2671
+ };
2672
+ }
2673
+ function flattenPaths(node, parent = IDENTITY_MATRIX) {
2674
+ const m = multiplyMatrices(nodeMatrix(node), parent);
2675
+ if (node.type === "path") return [{ path: node, m, opacity: 1 }];
2676
+ const o = node.opacity ?? 1;
2677
+ return node.children.filter((c) => c.visible !== false).flatMap((c) => flattenPaths(c, m)).map((e) => ({ ...e, opacity: e.opacity * o }));
2678
+ }
2679
+ var placedBounds = (path, m) => boundsOfPoints(contoursOf(path, m).flatMap((c) => c.pts));
2680
+ function collapsedPath(path, m, from, to) {
2681
+ const own = placedBounds(path, m);
2682
+ const c = own ? [own.x + own.width / 2, own.y + own.height / 2] : [0, 0];
2683
+ const fx = from && from.width > 0 ? (c[0] - from.x) / from.width : 0.5;
2684
+ const fy = from && from.height > 0 ? (c[1] - from.y) / from.height : 0.5;
2685
+ const p = to ? { x: to.x + to.width * fx, y: to.y + to.height * fy } : { x: c[0], y: c[1] };
2686
+ const subpaths = path.subpaths.map((sp) => ({ closed: sp.closed, anchors: sp.anchors.map(() => ({ x: p.x, y: p.y })) }));
2687
+ const { transform: _t, ...rest } = path;
2688
+ return { ...rest, subpaths };
2689
+ }
2690
+ function createVectorMorph(a, b, options = {}) {
2691
+ if (a.type === "path" && b.type === "path") {
2692
+ const at2 = preparePathMorph(a, b, options);
2693
+ return { at: at2 };
2694
+ }
2695
+ const pa = flattenPaths(a);
2696
+ const pb = flattenPaths(b);
2697
+ const ba = unionBounds(pa.map((e) => placedBounds(e.path, e.m)));
2698
+ const bb = unionBounds(pb.map((e) => placedBounds(e.path, e.m)));
2699
+ const id = options.id ?? a.id;
2700
+ const parts = [];
2701
+ const opacities = [];
2702
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
2703
+ const x = pa[i];
2704
+ const y = pb[i];
2705
+ const sub3 = { ...options, id: `${id}-${i}`, starts: void 0 };
2706
+ if (x && y) {
2707
+ parts.push(preparePathMorph(x.path, y.path, sub3, x.m, y.m));
2708
+ opacities.push([x.opacity, y.opacity]);
2709
+ } else if (x) {
2710
+ parts.push(preparePathMorph(x.path, collapsedPath(x.path, x.m, ba, bb), sub3, x.m, IDENTITY_MATRIX));
2711
+ opacities.push([x.opacity, x.opacity]);
2712
+ } else if (y) {
2713
+ parts.push(preparePathMorph(collapsedPath(y.path, y.m, bb, ba), y.path, sub3, IDENTITY_MATRIX, y.m));
2714
+ opacities.push([y.opacity, y.opacity]);
2715
+ }
2716
+ }
2717
+ return {
2718
+ at: (t) => {
2719
+ const children = parts.map((p, i) => {
2720
+ const n = p(t);
2721
+ const o = lerpN(opacities[i][0], opacities[i][1], t) * (n.opacity ?? 1);
2722
+ if (o < 1) n.opacity = o;
2723
+ else delete n.opacity;
2724
+ return n;
2725
+ });
2726
+ const group = { id, type: "group", children };
2727
+ return group;
2728
+ }
2729
+ };
2730
+ }
2731
+ function interpolateVectorPaths(a, b, t, options = {}) {
2732
+ return createVectorMorph(a, b, options).at(t);
2733
+ }
2734
+ var keyframeMorphs = /* @__PURE__ */ new WeakMap();
2735
+ function evaluateVectorPathKeyframes(keyframes, time, options = {}) {
2736
+ const keys = keyframes.filter((k) => Number.isFinite(k.time)).sort((x, y) => x.time - y.time);
2737
+ if (!keys.length) return null;
2738
+ if (!(time > keys[0].time)) return keys[0].value;
2739
+ const last = keys[keys.length - 1];
2740
+ if (time >= last.time) return last.value;
2741
+ let i = 0;
2742
+ while (i + 1 < keys.length && keys[i + 1].time <= time) i++;
2743
+ const a = keys[i];
2744
+ const b = keys[i + 1];
2745
+ if (time === a.time) return a.value;
2746
+ if (a.easing === "hold") return a.value;
2747
+ const span = b.time - a.time;
2748
+ const u = Math.min(1, Math.max(0, evaluateEasing(a.easing, (time - a.time) / span, span)));
2749
+ let inner = keyframeMorphs.get(a.value);
2750
+ if (!inner) keyframeMorphs.set(a.value, inner = /* @__PURE__ */ new WeakMap());
2751
+ let morph = inner.get(b.value);
2752
+ if (!morph) inner.set(b.value, morph = createVectorMorph(a.value, b.value, options));
2753
+ return morph.at(u);
2754
+ }
2755
+
2756
+ // src/vector/blendGroup.ts
2757
+ var DEFAULT_VECTOR_BLEND_STEPS = 5;
2758
+ var DEFAULT_VECTOR_BLEND_DISTANCE = 20;
2759
+ var VECTOR_BLEND_MAX_STEPS = 1e3;
2760
+ var VECTOR_BLEND_MAX_SMOOTH_STEPS = 256;
2761
+ function isVectorBlendGroup(node) {
2762
+ return !!node && node.type === "group" && !!node.blend;
2763
+ }
2764
+ function vectorBlendKeys(group) {
2765
+ return group.children.filter((c) => !c.blendStep);
2766
+ }
2767
+ function resolveVectorBlendOptions(options) {
2768
+ return {
2769
+ spacing: options?.spacing ?? "steps",
2770
+ steps: options?.steps ?? DEFAULT_VECTOR_BLEND_STEPS,
2771
+ distance: options?.distance ?? DEFAULT_VECTOR_BLEND_DISTANCE,
2772
+ orientToSpine: options?.orientToSpine ?? false,
2773
+ linear: options?.linear ?? false
2774
+ };
2775
+ }
2776
+ function paintColors(p) {
2777
+ if (typeof p === "string") return [p];
2778
+ if (p && typeof p === "object") return p.stops.map((s) => s.color);
2779
+ return [];
2780
+ }
2781
+ function vectorBlendColorDistance(a, b) {
2782
+ const pa = collectPathNodes(a)[0];
2783
+ const pb = collectPathNodes(b)[0];
2784
+ if (!pa || !pb) return 0;
2785
+ let d = 0;
2786
+ for (const key of ["fill", "stroke"]) {
2787
+ const ca = paintColors(pa[key]);
2788
+ const cb = paintColors(pb[key]);
2789
+ ca.forEach((c, i) => {
2790
+ const other = cb[i] ?? cb[0];
2791
+ if (other === void 0) return;
2792
+ const x = hexToRgba(c);
2793
+ const y = hexToRgba(other);
2794
+ if (!x || !y) return;
2795
+ 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));
2796
+ });
2797
+ }
2798
+ return d;
2799
+ }
2800
+ var centreOf = (n) => {
2801
+ const b = vectorNodeBounds(n);
2802
+ return b ? [b.x + b.width / 2, b.y + b.height / 2] : [0, 0];
2803
+ };
2804
+ function vectorBlendStepCount(a, b, options, length2) {
2805
+ const o = resolveVectorBlendOptions(options);
2806
+ if (o.spacing === "distance") {
2807
+ const d = Math.max(0.01, o.distance);
2808
+ return Math.min(VECTOR_BLEND_MAX_STEPS, Math.max(0, Math.round(length2 / d) - 1));
2809
+ }
2810
+ if (o.spacing === "smoothColor") {
2811
+ const cd = vectorBlendColorDistance(a, b);
2812
+ if (cd > 1 / 255) return Math.min(VECTOR_BLEND_MAX_SMOOTH_STEPS, Math.max(1, Math.ceil(cd * 255 / 2)));
2813
+ const ba = vectorNodeBounds(a);
2814
+ const bb = vectorNodeBounds(b);
2815
+ 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;
2816
+ return Math.min(VECTOR_BLEND_MAX_SMOOTH_STEPS, Math.max(1, Math.ceil(dist4 / 2)));
2817
+ }
2818
+ return Math.min(VECTOR_BLEND_MAX_STEPS, Math.max(1, Math.round(Number.isFinite(o.steps) ? o.steps : DEFAULT_VECTOR_BLEND_STEPS)));
2819
+ }
2820
+ function vectorBlendRail(sp) {
2821
+ const pts = [];
2822
+ const angles = [];
2823
+ const push = (p, tangent2) => {
2824
+ const last = pts[pts.length - 1];
2825
+ if (last && Math.hypot(p[0] - last[0], p[1] - last[1]) < 1e-9) return;
2826
+ pts.push(p);
2827
+ angles.push(Math.atan2(tangent2[1], tangent2[0]));
2828
+ };
2829
+ const count = subpathSegmentCount(sp);
2830
+ for (let i = 0; i < count; i++) {
2831
+ const seg = subpathSegment(sp, i);
2832
+ const samples = seg.length === 2 ? 1 : 32;
2833
+ if (i === 0) push(seg[0], segmentTangent(seg, 0));
2834
+ for (let k = 1; k <= samples; k++) push(segmentPoint(seg, k / samples), segmentTangent(seg, k / samples));
2835
+ }
2836
+ if (pts.length < 2) return null;
2837
+ const cum = [0];
2838
+ 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]));
2839
+ return { pts, cum, angles, total: cum[cum.length - 1], closed: sp.closed };
2840
+ }
2841
+ function vectorBlendRailAt(rail, s) {
2842
+ let len2 = Number.isFinite(s) ? s : 0;
2843
+ if (rail.closed && rail.total > 0) len2 = (len2 % rail.total + rail.total) % rail.total;
2844
+ len2 = Math.min(rail.total, Math.max(0, len2));
2845
+ let lo = 0;
2846
+ let hi = rail.cum.length - 1;
2847
+ while (hi - lo > 1) {
2848
+ const mid = lo + hi >> 1;
2849
+ if (rail.cum[mid] <= len2) lo = mid;
2850
+ else hi = mid;
2851
+ }
2852
+ const span = rail.cum[hi] - rail.cum[lo];
2853
+ const u = span > 1e-12 ? (len2 - rail.cum[lo]) / span : 0;
2854
+ const a = rail.pts[lo];
2855
+ const b = rail.pts[hi];
2856
+ 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]) };
2857
+ }
2858
+ function vectorBlendSpine(group) {
2859
+ const keys = vectorBlendKeys(group);
2860
+ if (keys.length < 2) return null;
2861
+ const stored = group.blend?.spine;
2862
+ if (stored && stored.anchors.length >= 2) {
2863
+ const rail2 = vectorBlendRail(stored);
2864
+ if (rail2) {
2865
+ const end = rail2.closed ? rail2.total * (1 - 1 / keys.length) : rail2.total;
2866
+ return { spine: stored, rail: rail2, stops: keys.map((_, i) => end * i / (keys.length - 1)), replaced: true };
2867
+ }
2868
+ }
2869
+ const centres = keys.map(centreOf);
2870
+ const spine = { closed: false, anchors: centres.map((c) => ({ x: c[0], y: c[1] })) };
2871
+ const stops = [0];
2872
+ 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]));
2873
+ const rail = vectorBlendRail(spine) ?? { pts: centres, cum: stops, angles: centres.map(() => 0), total: stops[stops.length - 1], closed: false };
2874
+ return { spine, rail, stops, replaced: false };
2875
+ }
2876
+ function vectorBlendKeyTargets(group) {
2877
+ const s = vectorBlendSpine(group);
2878
+ return s ? s.stops.map((stop) => vectorBlendRailAt(s.rail, stop).point) : [];
2879
+ }
2880
+ var morphCache = /* @__PURE__ */ new WeakMap();
2881
+ function morphFor(a, b, blend) {
2882
+ const starts = [blend.starts?.[a.id] ?? null, blend.starts?.[b.id] ?? null];
2883
+ const key = JSON.stringify([!!blend.linear, starts]);
2884
+ let inner = morphCache.get(a);
2885
+ if (!inner) morphCache.set(a, inner = /* @__PURE__ */ new WeakMap());
2886
+ let byKey = inner.get(b);
2887
+ if (!byKey) inner.set(b, byKey = /* @__PURE__ */ new Map());
2888
+ let morph = byKey.get(key);
2889
+ if (!morph) byKey.set(key, morph = createVectorMorph(a, b, { linear: blend.linear, starts }));
2890
+ return morph;
2891
+ }
2892
+ var wrapAngle = (a) => Math.atan2(Math.sin(a), Math.cos(a));
2893
+ function asStep(node, id) {
2894
+ const rename = (n, nid) => n.type === "group" ? { ...n, id: nid, children: n.children.map((c, i) => rename(c, `${nid}.${i}`)) } : { ...n, id: nid };
2895
+ return { ...rename(node, id), blendStep: true, locked: true };
2896
+ }
2897
+ function generateVectorBlendChildren(group) {
2898
+ const keys = vectorBlendKeys(group);
2899
+ const blend = group.blend;
2900
+ if (!blend || keys.length < 2) return keys;
2901
+ const spine = vectorBlendSpine(group);
2902
+ if (!spine) return keys;
2903
+ const out = [];
2904
+ let counter = 0;
2905
+ keys.forEach((a, i) => {
2906
+ out.push(a);
2907
+ const b = keys[i + 1];
2908
+ if (!b) return;
2909
+ const s0 = spine.stops[i];
2910
+ const s1 = spine.stops[i + 1];
2911
+ const n = vectorBlendStepCount(a, b, blend, Math.abs(s1 - s0));
2912
+ const morph = morphFor(a, b, blend);
2913
+ const p0 = vectorBlendRailAt(spine.rail, s0);
2914
+ const p1 = vectorBlendRailAt(spine.rail, s1);
2915
+ for (let j = 1; j <= n; j++) {
2916
+ const t = j / (n + 1);
2917
+ let step = morph.at(t);
2918
+ if (spine.replaced) {
2919
+ const p = vectorBlendRailAt(spine.rail, s0 + (s1 - s0) * t);
2920
+ const dx = p.point[0] - (p0.point[0] + (p1.point[0] - p0.point[0]) * t);
2921
+ const dy = p.point[1] - (p0.point[1] + (p1.point[1] - p0.point[1]) * t);
2922
+ let m = Math.hypot(dx, dy) > 1e-9 ? translateMatrix(dx, dy) : IDENTITY_MATRIX;
2923
+ if (blend.orientToSpine) {
2924
+ const turn = wrapAngle(p.angle - (p0.angle + wrapAngle(p1.angle - p0.angle) * t));
2925
+ if (Math.abs(turn) > 1e-9) {
2926
+ const c = centreOf(step);
2927
+ const origin = { x: c[0] + m[4], y: c[1] + m[5] };
2928
+ m = multiplyMatrices(m, rotateMatrix(turn * 180 / Math.PI, origin));
2929
+ }
2930
+ }
2931
+ if (m !== IDENTITY_MATRIX) step = transformVectorNode(step, m);
2932
+ }
2933
+ out.push(asStep(step, `${group.id}~${++counter}`));
2934
+ }
2935
+ });
2936
+ return out;
2937
+ }
2938
+ var childrenCache = /* @__PURE__ */ new WeakMap();
2939
+ var sameList = (a, b) => a.length === b.length && a.every((n, i) => n === b[i]);
2940
+ function blendChildren(group) {
2941
+ const keys = vectorBlendKeys(group);
2942
+ const sig = `${group.id}|${JSON.stringify(group.blend)}`;
2943
+ const cached = keys[0] ? childrenCache.get(keys[0]) : void 0;
2944
+ let children;
2945
+ if (cached && cached.sig === sig && sameList(cached.keys, keys)) children = cached.children;
2946
+ else {
2947
+ children = generateVectorBlendChildren(group);
2948
+ if (keys[0]) childrenCache.set(keys[0], { sig, keys, children });
2949
+ }
2950
+ return sameList(children, group.children) ? group.children : children;
2951
+ }
2952
+ function refreshVectorBlends(nodes) {
2953
+ let changed = false;
2954
+ const out = nodes.map((n) => {
2955
+ if (n.type !== "group") return n;
2956
+ const inner = refreshVectorBlends(n.children);
2957
+ let next = inner === n.children ? n : { ...n, children: inner };
2958
+ if (next.blend) {
2959
+ const children = blendChildren(next);
2960
+ if (children !== next.children) next = { ...next, children };
2961
+ }
2962
+ if (next !== n) changed = true;
2963
+ return next;
2964
+ });
2965
+ return changed ? out : nodes;
2966
+ }
2967
+ function plain2(node, newId) {
2968
+ const { blendStep: _s, locked: _l, ...rest } = node;
2969
+ const renamed = { ...rest, id: newId() };
2970
+ if (renamed.type === "group") renamed.children = renamed.children.map((c) => plain2(c, newId));
2971
+ return renamed;
2972
+ }
2973
+ function expandVectorBlend(group, newId) {
2974
+ const { blend: _b, ...rest } = group;
2975
+ return { ...rest, children: group.children.map((c) => c.blendStep ? plain2(c, newId) : c) };
2976
+ }
2977
+ function releaseVectorBlend(group, newId) {
2978
+ const m = nodeMatrix(group);
2979
+ const opacity = group.opacity;
2980
+ 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);
2981
+ const spine = group.blend?.spine;
2982
+ if (!spine || spine.anchors.length < 2) return keys;
2983
+ const path = { id: newId(), type: "path", name: "Spine", fill: null, stroke: null, subpaths: [{ closed: spine.closed, anchors: spine.anchors.map((a) => ({ ...a })) }] };
2984
+ return [...keys, group.transform ? transformVectorNode(path, m) : path];
2985
+ }
2986
+
2028
2987
  // src/vector/snap.ts
2029
2988
  var VECTOR_CONSTRUCTION_ANGLES = [0, 45, 90, 135];
2030
2989
  function constrainToLeave(leave, p) {
@@ -2554,14 +3513,14 @@ var VectorSnapTargets = class _VectorSnapTargets {
2554
3513
  * corner) slides along it until one of them is on a target. An axis that snaps to nothing
2555
3514
  * may match another object's width or height instead.
2556
3515
  */
2557
- snapScale(frame, handle, sx, sy, mods, tol) {
2558
- const toDoc = frame.angle ? rotateMatrix(frame.angle) : IDENTITY_MATRIX;
2559
- const toLocal = frame.angle ? rotateMatrix(-frame.angle) : IDENTITY_MATRIX;
3516
+ snapScale(frame2, handle, sx, sy, mods, tol) {
3517
+ const toDoc = frame2.angle ? rotateMatrix(frame2.angle) : IDENTITY_MATRIX;
3518
+ const toLocal = frame2.angle ? rotateMatrix(-frame2.angle) : IDENTITY_MATRIX;
2560
3519
  const corner = handle.length === 2;
2561
3520
  const proportional = !!mods.shift;
2562
- const centre = { x: frame.x + frame.width / 2, y: frame.y + frame.height / 2 };
2563
- const origin = mods.alt ? centre : handlePos(opposite(handle), frame);
2564
- const hp = handlePos(handle, frame);
3521
+ const centre = { x: frame2.x + frame2.width / 2, y: frame2.y + frame2.height / 2 };
3522
+ const origin = mods.alt ? centre : handlePos(opposite(handle), frame2);
3523
+ const hp = handlePos(handle, frame2);
2565
3524
  const d0 = { x: hp.x - origin.x, y: hp.y - origin.y };
2566
3525
  const valid = (n, s) => Number.isFinite(n) && n * s > 0;
2567
3526
  const nearest = (v2, lines) => {
@@ -2587,7 +3546,7 @@ var VectorSnapTargets = class _VectorSnapTargets {
2587
3546
  const q = applyMatrix(toLocal, { x: tx ?? p.x, y: ty ?? p.y });
2588
3547
  let cx = (q.x - origin.x) / d0.x;
2589
3548
  let cy = (q.y - origin.y) / d0.y;
2590
- if (!frame.angle) {
3549
+ if (!frame2.angle) {
2591
3550
  if (tx === null) cx = sx;
2592
3551
  if (ty === null) cy = sy;
2593
3552
  }
@@ -2616,7 +3575,7 @@ var VectorSnapTargets = class _VectorSnapTargets {
2616
3575
  if (!best || (inLine === best.inLine ? moved < best.moved : !inLine)) best = { s, inLine, moved, snap: found2 };
2617
3576
  };
2618
3577
  for (const h of [handle, ...ends]) {
2619
- const hv = handlePos(h, frame);
3578
+ const hv = handlePos(h, frame2);
2620
3579
  const v2 = { x: hv.x - origin.x, y: hv.y - origin.y };
2621
3580
  const base = applyMatrix(toDoc, { x: origin.x + c.x * v2.x, y: origin.y + c.y * v2.y });
2622
3581
  const dir = applyMatrix(toDoc, { x: e.x * v2.x, y: e.y * v2.y });
@@ -2651,25 +3610,25 @@ var VectorSnapTargets = class _VectorSnapTargets {
2651
3610
  const movesY = handle !== "e" && handle !== "w";
2652
3611
  const sized = [];
2653
3612
  if (!proportional) {
2654
- if (movesX && !snap.hx && !(frame.angle && snap.hy) && frame.width > 1e-9) {
2655
- const len2 = this.matchSize(Math.abs(out.sx) * frame.width, tol);
3613
+ if (movesX && !snap.hx && !(frame2.angle && snap.hy) && frame2.width > 1e-9) {
3614
+ const len2 = this.matchSize(Math.abs(out.sx) * frame2.width, tol);
2656
3615
  if (len2 !== null && len2 > 0) {
2657
- out.sx = Math.sign(out.sx || 1) * (len2 / frame.width);
3616
+ out.sx = Math.sign(out.sx || 1) * (len2 / frame2.width);
2658
3617
  sized.push("x");
2659
3618
  }
2660
3619
  }
2661
- if (movesY && !snap.hy && !(frame.angle && snap.hx) && frame.height > 1e-9) {
2662
- const len2 = this.matchSize(Math.abs(out.sy) * frame.height, tol);
3620
+ if (movesY && !snap.hy && !(frame2.angle && snap.hx) && frame2.height > 1e-9) {
3621
+ const len2 = this.matchSize(Math.abs(out.sy) * frame2.height, tol);
2663
3622
  if (len2 !== null && len2 > 0) {
2664
- out.sy = Math.sign(out.sy || 1) * (len2 / frame.height);
3623
+ out.sy = Math.sign(out.sy || 1) * (len2 / frame2.height);
2665
3624
  sized.push("y");
2666
3625
  }
2667
3626
  }
2668
3627
  }
2669
- const x0 = origin.x + (frame.x - origin.x) * out.sx;
2670
- const x1 = origin.x + (frame.x + frame.width - origin.x) * out.sx;
2671
- const y0 = origin.y + (frame.y - origin.y) * out.sy;
2672
- const y1 = origin.y + (frame.y + frame.height - origin.y) * out.sy;
3628
+ const x0 = origin.x + (frame2.x - origin.x) * out.sx;
3629
+ const x1 = origin.x + (frame2.x + frame2.width - origin.x) * out.sx;
3630
+ const y0 = origin.y + (frame2.y - origin.y) * out.sy;
3631
+ const y1 = origin.y + (frame2.y + frame2.height - origin.y) * out.sy;
2673
3632
  const box = { x0: Math.min(x0, x1), x1: Math.max(x0, x1), y0: Math.min(y0, y1), y1: Math.max(y0, y1) };
2674
3633
  const page = [
2675
3634
  { x: box.x0, y: box.y0 },
@@ -3958,6 +4917,15 @@ var DEFAULT_VECTOR_PATH_EDIT = {
3958
4917
  fillGap: 0
3959
4918
  };
3960
4919
  var VECTOR_PENCIL_STRING_PX = 100;
4920
+ function blendOptionsOf(input) {
4921
+ const out = {};
4922
+ if (input.spacing === "steps" || input.spacing === "distance" || input.spacing === "smoothColor") out.spacing = input.spacing;
4923
+ if (typeof input.steps === "number" && Number.isFinite(input.steps)) out.steps = Math.min(1e3, Math.max(1, Math.round(input.steps)));
4924
+ if (typeof input.distance === "number" && Number.isFinite(input.distance) && input.distance > 0) out.distance = input.distance;
4925
+ if (typeof input.orientToSpine === "boolean") out.orientToSpine = input.orientToSpine;
4926
+ if (typeof input.linear === "boolean") out.linear = input.linear;
4927
+ return out;
4928
+ }
3961
4929
  var VECTOR_STYLE_KEYS = [
3962
4930
  "fill",
3963
4931
  "stroke",
@@ -4078,6 +5046,12 @@ function vectorStrokeWidthGeometry(node) {
4078
5046
  const parts = vectorStrokeParts(subpathsToBezier(node.subpaths), node);
4079
5047
  return parts.line.length ? { line: parts.line, unit: parts.drawWidth / 2 } : null;
4080
5048
  }
5049
+ function shiftWidthPoints(profile, indices, dt) {
5050
+ const moving = new Set(indices);
5051
+ const tagged = profile.map((p, i) => ({ p: moving.has(i) ? { ...p, t: Math.min(Math.max(p.t + dt, 0), 1) } : { ...p }, i }));
5052
+ tagged.sort((a, b) => a.p.t - b.p.t || a.i - b.i);
5053
+ return { points: tagged.map((x) => x.p), renumber: new Map(tagged.map((x, k) => [x.i, k])) };
5054
+ }
4081
5055
  function getVectorWidthHandles(state) {
4082
5056
  if (state.tool !== "width") return [];
4083
5057
  const ids = /* @__PURE__ */ new Set();
@@ -4087,7 +5061,7 @@ function getVectorWidthHandles(state) {
4087
5061
  }
4088
5062
  if (state.hover?.nodeId) ids.add(state.hover.nodeId);
4089
5063
  if (state.interaction?.type === "width") ids.add(state.interaction.ref.nodeId);
4090
- if (state.selectedWidthPoint) ids.add(state.selectedWidthPoint.nodeId);
5064
+ for (const r of state.selectedWidthPoints) ids.add(r.nodeId);
4091
5065
  const out = [];
4092
5066
  for (const id of ids) {
4093
5067
  const node = findVectorNode(state.document.nodes, id);
@@ -4106,7 +5080,6 @@ function getVectorWidthHandles(state) {
4106
5080
  const p = applyMatrix(m, c);
4107
5081
  const l = applyMatrix(m, { x: c.x + n.x * w.left * g.unit, y: c.y + n.y * w.left * g.unit });
4108
5082
  const r = applyMatrix(m, { x: c.x - n.x * w.right * g.unit, y: c.y - n.y * w.right * g.unit });
4109
- const sel = state.selectedWidthPoint;
4110
5083
  out.push({
4111
5084
  ref: { nodeId: id, index, subpath: sp },
4112
5085
  x: p.x,
@@ -4116,7 +5089,7 @@ function getVectorWidthHandles(state) {
4116
5089
  t: w.t,
4117
5090
  leftScale: w.left,
4118
5091
  rightScale: w.right,
4119
- selected: !!sel && sel.nodeId === id && sel.index === index
5092
+ selected: state.selectedWidthPoints.some((r2) => r2.nodeId === id && r2.index === index)
4120
5093
  });
4121
5094
  });
4122
5095
  }
@@ -4159,11 +5132,11 @@ function getTransformHandlePositions(box) {
4159
5132
  { handle: "rotate", x: x + w / 2, y: y - VECTOR_ROTATE_HANDLE_OFFSET }
4160
5133
  ];
4161
5134
  }
4162
- function getVectorSelectionBoxGeometry(frame, viewport) {
5135
+ function getVectorSelectionBoxGeometry(frame2, viewport) {
4163
5136
  const z = viewport.zoom;
4164
- const box = { x: frame.x * z + viewport.x, y: frame.y * z + viewport.y, width: frame.width * z, height: frame.height * z };
4165
- const rot = rotateMatrix(frame.angle, { x: viewport.x, y: viewport.y });
4166
- const toScreen2 = (p) => frame.angle ? applyMatrix(rot, p) : { x: p.x, y: p.y };
5137
+ const box = { x: frame2.x * z + viewport.x, y: frame2.y * z + viewport.y, width: frame2.width * z, height: frame2.height * z };
5138
+ const rot = rotateMatrix(frame2.angle, { x: viewport.x, y: viewport.y });
5139
+ const toScreen2 = (p) => frame2.angle ? applyMatrix(rot, p) : { x: p.x, y: p.y };
4167
5140
  const handles = getTransformHandlePositions(box).map((h) => ({ ...h, screen: toScreen2(h) }));
4168
5141
  const corners2 = [
4169
5142
  { x: box.x, y: box.y },
@@ -4172,7 +5145,7 @@ function getVectorSelectionBoxGeometry(frame, viewport) {
4172
5145
  { x: box.x, y: box.y + box.height }
4173
5146
  ].map(toScreen2);
4174
5147
  const r = (n) => Math.round(n * 1e3) / 1e3;
4175
- return { box, angle: frame.angle, transform: frame.angle ? `rotate(${r(frame.angle)} ${r(viewport.x)} ${r(viewport.y)})` : "", handles, corners: corners2 };
5148
+ return { box, angle: frame2.angle, transform: frame2.angle ? `rotate(${r(frame2.angle)} ${r(viewport.x)} ${r(viewport.y)})` : "", handles, corners: corners2 };
4176
5149
  }
4177
5150
  var RESIZE_CURSOR_ORDER = ["ew-resize", "nwse-resize", "ns-resize", "nesw-resize"];
4178
5151
  var HANDLE_ANGLES = { e: 0, se: 45, s: 90, sw: 135, w: 180, nw: 225, n: 270, ne: 315 };
@@ -4196,7 +5169,8 @@ var TOOL_LABELS = {
4196
5169
  eraser: "Eraser",
4197
5170
  width: "Width",
4198
5171
  shapeBuilder: "Shape Builder",
4199
- paintBucket: "Paint Bucket"
5172
+ paintBucket: "Paint Bucket",
5173
+ blend: "Blend"
4200
5174
  };
4201
5175
  var TOOL_KEYS = {
4202
5176
  v: "select",
@@ -4213,7 +5187,8 @@ var TOOL_KEYS = {
4213
5187
  e: "eraser",
4214
5188
  w: "width",
4215
5189
  m: "shapeBuilder",
4216
- b: "paintBucket"
5190
+ b: "paintBucket",
5191
+ j: "blend"
4217
5192
  };
4218
5193
  function vectorToolLabel(tool) {
4219
5194
  return TOOL_LABELS[tool];
@@ -4233,7 +5208,8 @@ var VECTOR_TOOL_SHORTCUTS = {
4233
5208
  eraser: "E",
4234
5209
  width: "W",
4235
5210
  shapeBuilder: "M",
4236
- paintBucket: "B"
5211
+ paintBucket: "B",
5212
+ blend: "J"
4237
5213
  };
4238
5214
  var VECTOR_TOOL_GLYPHS = {
4239
5215
  select: "\u2196",
@@ -4250,7 +5226,8 @@ var VECTOR_TOOL_GLYPHS = {
4250
5226
  eraser: "\u232B",
4251
5227
  width: "\u21F9",
4252
5228
  shapeBuilder: "\u2295",
4253
- paintBucket: "\u25CD"
5229
+ paintBucket: "\u25CD",
5230
+ blend: "\u29D3"
4254
5231
  };
4255
5232
  var SHAPE_TOOLS = /* @__PURE__ */ new Set(["rect", "ellipse", "polygon", "star", "line"]);
4256
5233
  var BOOLEAN_OPS = {
@@ -4312,7 +5289,8 @@ var VectorEditorController = class {
4312
5289
  doc;
4313
5290
  selection = [];
4314
5291
  anchors = [];
4315
- widthPoint = null;
5292
+ widthPoints = [];
5293
+ widthEdit = null;
4316
5294
  editingId = null;
4317
5295
  pen = null;
4318
5296
  tool;
@@ -4337,6 +5315,9 @@ var VectorEditorController = class {
4337
5315
  /** Paint bucket map of the visible outlines. */
4338
5316
  fillCache = null;
4339
5317
  builderPreview = null;
5318
+ blendStart = null;
5319
+ /** Options new blends get (the toolbars edit them along with the selected blend's). */
5320
+ blendDefaults = {};
4340
5321
  /** What the highlight shows, to skip notifying when the pointer stays over the same face. */
4341
5322
  builderHoverKey = null;
4342
5323
  createId;
@@ -4351,6 +5332,7 @@ var VectorEditorController = class {
4351
5332
  constructor(options = {}) {
4352
5333
  this.history = options.history ?? new HistoryManager();
4353
5334
  this.doc = options.document ? cloneVectorDocument(options.document) : { width: 800, height: 600, nodes: [] };
5335
+ this.doc = this.refreshed(this.doc);
4354
5336
  this.tool = options.tool ?? "select";
4355
5337
  this.defaults = { ...DEFAULT_DEFAULTS, ...options.defaults ?? {} };
4356
5338
  this.viewport = { x: 0, y: 0, zoom: 1, ...options.viewport ?? {} };
@@ -4398,7 +5380,8 @@ var VectorEditorController = class {
4398
5380
  tool: this.tool,
4399
5381
  selection: this.selection,
4400
5382
  selectedAnchors: this.anchors,
4401
- selectedWidthPoint: this.widthPoint,
5383
+ selectedWidthPoints: this.widthPoints,
5384
+ widthPointEdit: this.widthEdit,
4402
5385
  editingId: this.editingId,
4403
5386
  selectionBounds: this.boundsOf(this.selection),
4404
5387
  selectionFrame: this.frameOf(this.selectedRoots()),
@@ -4414,6 +5397,8 @@ var VectorEditorController = class {
4414
5397
  pen: this.pen,
4415
5398
  pointer: this.pointer,
4416
5399
  builder: this.builderPreview,
5400
+ blendStart: this.blendStart,
5401
+ blendDefaults: this.blendDefaults,
4417
5402
  canUndo: h.canUndo,
4418
5403
  canRedo: h.canRedo,
4419
5404
  undoLabel: h.undoName,
@@ -4421,7 +5406,15 @@ var VectorEditorController = class {
4421
5406
  revision: this.revision
4422
5407
  };
4423
5408
  }
5409
+ /** `doc` with its blends' steps brought up to date (the same object when they already are). */
5410
+ refreshed(doc) {
5411
+ const nodes = refreshVectorBlends(doc.nodes);
5412
+ return nodes === doc.nodes ? doc : { ...doc, nodes };
5413
+ }
4424
5414
  notify(docChanged = false) {
5415
+ const fresh = this.refreshed(this.doc);
5416
+ if (fresh !== this.doc) this.doc = fresh;
5417
+ if (this.blendStart && !this.getNode(this.blendStart.nodeId)) this.blendStart = null;
4425
5418
  this.revision++;
4426
5419
  this.state = this.computeState();
4427
5420
  if (docChanged) this.onChange?.(this.doc);
@@ -4469,7 +5462,7 @@ var VectorEditorController = class {
4469
5462
  case "pen":
4470
5463
  return h?.anchor ? "pointer" : "crosshair";
4471
5464
  case "width":
4472
- if (this.drag?.kind === "width") return this.drag.mode === "move" ? "grabbing" : "ew-resize";
5465
+ if (this.drag?.kind === "width") return this.drag.mode === "width" ? "ew-resize" : "grabbing";
4473
5466
  if (h?.widthPoint) return h.widthPoint.part === "center" ? "grab" : "ew-resize";
4474
5467
  return h?.strokeSpot ? "copy" : "default";
4475
5468
  case "shapeBuilder":
@@ -4477,6 +5470,8 @@ var VectorEditorController = class {
4477
5470
  return this.builderPreview.mode === "erase" ? "not-allowed" : "cell";
4478
5471
  case "paintBucket":
4479
5472
  return this.builderPreview ? "cell" : "crosshair";
5473
+ case "blend":
5474
+ return h?.nodeId ? this.blendStart && this.blendStart.nodeId !== h.nodeId ? "copy" : "pointer" : "crosshair";
4480
5475
  default:
4481
5476
  return "crosshair";
4482
5477
  }
@@ -4489,6 +5484,7 @@ var VectorEditorController = class {
4489
5484
  }
4490
5485
  restore(s) {
4491
5486
  const docChanged = s.doc !== this.doc || this.committing;
5487
+ if (!this.committing) this.widthEdit = null;
4492
5488
  this.committing = false;
4493
5489
  this.applying = true;
4494
5490
  try {
@@ -4497,6 +5493,13 @@ var VectorEditorController = class {
4497
5493
  this.anchors = s.anchors.filter((r) => getAnchor(this.doc, r));
4498
5494
  this.editingId = s.editingId && this.getNode(s.editingId) ? s.editingId : null;
4499
5495
  this.pen = s.pen && this.getNode(s.pen.nodeId)?.subpaths?.[s.pen.subpath] ? s.pen : null;
5496
+ if (this.widthPoints.length) {
5497
+ const kept = this.widthPoints.filter((r) => {
5498
+ const n = this.getNode(r.nodeId);
5499
+ return n?.type === "path" && !!n.strokeWidthProfile?.[r.index];
5500
+ });
5501
+ if (kept.length !== this.widthPoints.length) this.widthPoints = kept;
5502
+ }
4500
5503
  } finally {
4501
5504
  this.applying = false;
4502
5505
  }
@@ -4504,6 +5507,7 @@ var VectorEditorController = class {
4504
5507
  }
4505
5508
  /** Records the change from `before` to the current state as one undo step. */
4506
5509
  commit(name, before) {
5510
+ this.doc = this.refreshed(this.doc);
4507
5511
  const after = this.snapshot();
4508
5512
  if (before.doc === after.doc) {
4509
5513
  this.notify();
@@ -4602,9 +5606,11 @@ var VectorEditorController = class {
4602
5606
  this.tool = tool;
4603
5607
  this.hover = null;
4604
5608
  this.guides = null;
4605
- this.widthPoint = null;
5609
+ this.widthPoints = [];
5610
+ this.widthEdit = null;
4606
5611
  this.builderPreview = null;
4607
5612
  this.builderHoverKey = null;
5613
+ this.blendStart = null;
4608
5614
  if (tool !== "direct") {
4609
5615
  this.anchors = [];
4610
5616
  this.editingId = null;
@@ -4878,18 +5884,143 @@ var VectorEditorController = class {
4878
5884
  const p = profile;
4879
5885
  points = p.taper || p.wave ? generateWidthProfile(p) : [];
4880
5886
  }
4881
- this.widthPoint = null;
5887
+ this.widthPoints = [];
5888
+ this.widthEdit = null;
4882
5889
  return this.setStyle({ strokeWidthProfile: points.length ? points : null }, options.ids ?? this.selection, { label: points.length ? "Width Profile" : "Uniform Width" });
4883
5890
  }
4884
- /** Removes one width point of a path's profile (the width tool's Delete). */
5891
+ /** Removes one width point of a path's profile. */
4885
5892
  removeWidthPoint(ref) {
4886
- const node = this.getNode(ref.nodeId);
4887
- if (node?.type !== "path" || !node.strokeWidthProfile?.[ref.index]) return { ok: false, message: "There is no such width point." };
4888
- const rest = removeWidthPoints(sanitizeWidthProfile(node.strokeWidthProfile), [ref.index]);
4889
- this.widthPoint = null;
4890
- this.change("Delete Width Point", () => this.replaceNode(node.id, withStyle(node, { strokeWidthProfile: rest.length ? rest : null })));
5893
+ return this.removeWidthPoints([ref]);
5894
+ }
5895
+ /** Removes width points (default: the selected ones — the width tool's Delete), as one undo step. */
5896
+ removeWidthPoints(refs = this.widthPoints) {
5897
+ const byNode = /* @__PURE__ */ new Map();
5898
+ for (const r of refs) {
5899
+ const node = this.getNode(r.nodeId);
5900
+ if (node?.type !== "path" || !node.strokeWidthProfile?.[r.index]) continue;
5901
+ byNode.set(r.nodeId, [...byNode.get(r.nodeId) ?? [], r.index]);
5902
+ }
5903
+ if (!byNode.size) return { ok: false, message: "There is no such width point." };
5904
+ const count = [...byNode.values()].reduce((n, l) => n + new Set(l).size, 0);
5905
+ this.widthPoints = [];
5906
+ this.widthEdit = null;
5907
+ this.change(count > 1 ? "Delete Width Points" : "Delete Width Point", () => {
5908
+ for (const [id, indices] of byNode) {
5909
+ const node = this.getNode(id);
5910
+ const rest = removeWidthPoints(sanitizeWidthProfile(node.strokeWidthProfile), indices);
5911
+ this.replaceNode(id, withStyle(node, { strokeWidthProfile: rest.length ? rest : null }));
5912
+ }
5913
+ });
5914
+ return { ok: true };
5915
+ }
5916
+ /** Selects width points (width tool); points on other paths than the first are dropped. */
5917
+ selectWidthPoints(refs) {
5918
+ const first = refs[0]?.nodeId;
5919
+ const next = [];
5920
+ for (const r of refs) {
5921
+ if (r.nodeId !== first || next.some((q) => q.index === r.index)) continue;
5922
+ const n = this.getNode(r.nodeId);
5923
+ if (n?.type === "path" && n.strokeWidthProfile?.[r.index]) next.push({ nodeId: r.nodeId, index: r.index, subpath: r.subpath ?? 0 });
5924
+ }
5925
+ this.widthPoints = next;
5926
+ this.notify();
5927
+ }
5928
+ /**
5929
+ * Sets one width point numerically: `index` names an existing point of path `nodeId`, null
5930
+ * (or left out) adds one at `values.t`. `left` / `right` are side widths in document units
5931
+ * (how far the stroke reaches on each side of the path), `t` the position along each subpath
5932
+ * (0–1). A point moved onto another one's `t` makes a step there. Selects the point and
5933
+ * refreshes an open width-point editor; one undo step. Ported from ArtCraft vectorcraft's
5934
+ * Width Point Edit dialog (`crates/tools/src/distort/width.rs`, MIT OR Apache-2.0).
5935
+ */
5936
+ setWidthPoint(target, values) {
5937
+ const node = this.getNode(target.nodeId);
5938
+ if (node?.type !== "path") return { ok: false, message: "There is no such path." };
5939
+ const g = vectorStrokeWidthGeometry(node);
5940
+ if (!g || !(g.unit > 0)) return { ok: false, message: "The path has no visible stroke." };
5941
+ const num = (v2) => typeof v2 === "number" && Number.isFinite(v2) ? v2 : void 0;
5942
+ const t = num(values.t);
5943
+ const left = num(values.left);
5944
+ const right = num(values.right);
5945
+ const scale = (w) => w === void 0 ? void 0 : Math.max(0, w) / g.unit;
5946
+ const profile = sanitizeWidthProfile(node.strokeWidthProfile);
5947
+ let points;
5948
+ let index;
5949
+ const existing = target.index ?? null;
5950
+ if (existing === null) {
5951
+ if (t === void 0) return { ok: false, message: "A new width point needs a position t." };
5952
+ if (profile.length >= VECTOR_WIDTH_PROFILE_MAX_POINTS) return { ok: false, message: "The profile has too many width points." };
5953
+ ({ points, index } = insertWidthPoint(profile.length ? profile : null, t, { left: scale(left), right: scale(right) }, { step: true }));
5954
+ } else {
5955
+ const p = profile[existing];
5956
+ if (!p) return { ok: false, message: "There is no such width point." };
5957
+ const sides2 = { left: scale(left) ?? p.left, right: scale(right) ?? p.right };
5958
+ const tt = t === void 0 ? p.t : Math.min(Math.max(t, 0), 1);
5959
+ if (Math.abs(tt - p.t) <= VECTOR_WIDTH_SAME_T) {
5960
+ points = setWidthPoint(profile, existing, sides2);
5961
+ index = existing;
5962
+ } else {
5963
+ const rest = removeWidthPoints(profile, [existing]);
5964
+ if (rest.length) ({ points, index } = insertWidthPoint(rest, tt, sides2, { step: true }));
5965
+ else {
5966
+ points = [{ t: tt, ...sides2 }];
5967
+ index = 0;
5968
+ }
5969
+ }
5970
+ }
5971
+ const subpath = this.widthEdit?.nodeId === node.id ? this.widthEdit.subpath : this.widthPoints.find((r) => r.nodeId === node.id)?.subpath ?? 0;
5972
+ const edit = this.widthEdit;
5973
+ this.change("Width Point", () => this.replaceNode(node.id, withStyle(node, { strokeWidthProfile: points })));
5974
+ this.widthPoints = [{ nodeId: node.id, index, subpath }];
5975
+ if (edit?.nodeId === node.id && (edit.index === existing || existing === null)) this.widthEdit = this.widthEditFor(node.id, subpath, index, null);
5976
+ this.notify();
4891
5977
  return { ok: true };
4892
5978
  }
5979
+ /**
5980
+ * Opens the width-point editor (what a double-click with the width tool does) on a width
5981
+ * point, or on a spot of a stroke (`t` without `index`: applying it adds a point there).
5982
+ * Returns false when the path has no visible stroke.
5983
+ */
5984
+ openWidthPointEdit(target) {
5985
+ const edit = this.widthEditFor(target.nodeId, target.subpath ?? 0, target.index ?? null, target.t ?? null);
5986
+ if (!edit) return false;
5987
+ this.widthEdit = edit;
5988
+ this.widthPoints = edit.index === null ? [] : [{ nodeId: edit.nodeId, index: edit.index, subpath: edit.subpath }];
5989
+ this.notify();
5990
+ return true;
5991
+ }
5992
+ /** Closes the width-point editor without changing anything. */
5993
+ closeWidthPointEdit() {
5994
+ if (!this.widthEdit) return;
5995
+ this.widthEdit = null;
5996
+ this.notify();
5997
+ }
5998
+ /** Applies `values` to the width point being edited (see `setWidthPoint`) and closes the editor. */
5999
+ commitWidthPointEdit(values) {
6000
+ const edit = this.widthEdit;
6001
+ if (!edit) return { ok: false, message: "No width point is being edited." };
6002
+ const r = this.setWidthPoint({ nodeId: edit.nodeId, index: edit.index }, edit.index === null ? { t: edit.t, ...values } : values);
6003
+ if (r.ok) this.closeWidthPointEdit();
6004
+ return r;
6005
+ }
6006
+ /** The editor's view of point `index` (or of the stroke at `t` when index is null) of `nodeId`. */
6007
+ widthEditFor(nodeId, subpath, index, t) {
6008
+ const node = this.getNode(nodeId);
6009
+ if (node?.type !== "path") return null;
6010
+ const g = vectorStrokeWidthGeometry(node);
6011
+ if (!g) return null;
6012
+ const profile = sanitizeWidthProfile(node.strokeWidthProfile);
6013
+ const p = index === null ? null : profile[index];
6014
+ if (index !== null && !p) return null;
6015
+ const tt = p ? p.t : Math.min(Math.max(t ?? 0, 0), 1);
6016
+ const sides2 = p ?? widthProfileAt(profile.length ? profile : null, tt);
6017
+ const sp = Math.min(Math.max(0, subpath), Math.max(0, node.subpaths.length - 1));
6018
+ const at2 = strokePositionPoint(g.line, sp, tt);
6019
+ const round = (v2) => Math.round(v2 * 1e6) / 1e6;
6020
+ const q = at2 ? applyMatrix(this.worldOf(nodeId), { x: at2.point[0], y: at2.point[1] }) : { x: 0, y: 0 };
6021
+ const point = { x: round(q.x), y: round(q.y) };
6022
+ return { nodeId, subpath: sp, index, t: round(tt), left: round(sides2.left * g.unit), right: round(sides2.right * g.unit), point };
6023
+ }
4893
6024
  /**
4894
6025
  * Sets the live corner radius (and optionally the kind) of anchors: `refs`, else the selected
4895
6026
  * anchors (direct tool), else every anchor of the `ids` / selected paths. Radius 0 makes the
@@ -5104,24 +6235,24 @@ var VectorEditorController = class {
5104
6235
  return this.transform(translateMatrix(dx, dy), { ids: options.ids, label: "Move" });
5105
6236
  }
5106
6237
  /** The selection frame's centre in document space. */
5107
- frameCenter(frame) {
5108
- return applyMatrix(rotateMatrix(frame.angle), { x: frame.x + frame.width / 2, y: frame.y + frame.height / 2 });
6238
+ frameCenter(frame2) {
6239
+ return applyMatrix(rotateMatrix(frame2.angle), { x: frame2.x + frame2.width / 2, y: frame2.y + frame2.height / 2 });
5109
6240
  }
5110
6241
  /**
5111
6242
  * Scales along the selection box's own axes (rotated with the selection when its nodes
5112
6243
  * share a rotation) around `origin` (default: the box centre, in document space).
5113
6244
  */
5114
6245
  scale(sx, sy = sx, options = {}) {
5115
- const frame = this.frameOf(options.ids ?? this.selectedRoots());
5116
- if (!frame) return { ok: false, message: "Nothing is selected." };
5117
- const origin = applyMatrix(rotateMatrix(-frame.angle), options.origin ?? this.frameCenter(frame));
5118
- return this.transform(this.inFrame(frame.angle, scaleMatrix(sx, sy, origin)), { ids: options.ids, label: "Scale" });
6246
+ const frame2 = this.frameOf(options.ids ?? this.selectedRoots());
6247
+ if (!frame2) return { ok: false, message: "Nothing is selected." };
6248
+ const origin = applyMatrix(rotateMatrix(-frame2.angle), options.origin ?? this.frameCenter(frame2));
6249
+ return this.transform(this.inFrame(frame2.angle, scaleMatrix(sx, sy, origin)), { ids: options.ids, label: "Scale" });
5119
6250
  }
5120
6251
  /** Rotates clockwise by `degrees` around `origin` (default: the selection centre). */
5121
6252
  rotate(degrees, options = {}) {
5122
- const frame = this.frameOf(options.ids ?? this.selectedRoots());
5123
- if (!frame) return { ok: false, message: "Nothing is selected." };
5124
- const origin = options.origin ?? this.frameCenter(frame);
6253
+ const frame2 = this.frameOf(options.ids ?? this.selectedRoots());
6254
+ if (!frame2) return { ok: false, message: "Nothing is selected." };
6255
+ const origin = options.origin ?? this.frameCenter(frame2);
5125
6256
  return this.transform(rotateMatrix(degrees, origin), { ids: options.ids, label: "Rotate" });
5126
6257
  }
5127
6258
  /** Sets each selected (or given) node's rotation to `degrees`, turning it around its own centre. */
@@ -5130,21 +6261,21 @@ var VectorEditorController = class {
5130
6261
  if (!ids.length) return { ok: false, message: "Nothing is selected." };
5131
6262
  this.change("Set Rotation", () => {
5132
6263
  for (const id of ids) {
5133
- const frame = this.frameOf([id]);
5134
- if (!frame) continue;
5135
- const delta = normalizeAngle(degrees - frame.angle);
5136
- if (Math.abs(delta) > 1e-9) this.doc = this.transformed(this.doc, /* @__PURE__ */ new Set([id]), rotateMatrix(delta, this.frameCenter(frame)));
6264
+ const frame2 = this.frameOf([id]);
6265
+ if (!frame2) continue;
6266
+ const delta = normalizeAngle(degrees - frame2.angle);
6267
+ if (Math.abs(delta) > 1e-9) this.doc = this.transformed(this.doc, /* @__PURE__ */ new Set([id]), rotateMatrix(delta, this.frameCenter(frame2)));
5137
6268
  }
5138
6269
  });
5139
6270
  return { ok: true };
5140
6271
  }
5141
6272
  /** Mirrors along the selection box's own axes. */
5142
6273
  flip(axis, options = {}) {
5143
- const frame = this.frameOf(options.ids ?? this.selectedRoots());
5144
- if (!frame) return { ok: false, message: "Nothing is selected." };
5145
- const c = { x: frame.x + frame.width / 2, y: frame.y + frame.height / 2 };
6274
+ const frame2 = this.frameOf(options.ids ?? this.selectedRoots());
6275
+ if (!frame2) return { ok: false, message: "Nothing is selected." };
6276
+ const c = { x: frame2.x + frame2.width / 2, y: frame2.y + frame2.height / 2 };
5146
6277
  const m = axis === "horizontal" ? scaleMatrix(-1, 1, c) : scaleMatrix(1, -1, c);
5147
- return this.transform(this.inFrame(frame.angle, m), { ids: options.ids, label: axis === "horizontal" ? "Flip Horizontal" : "Flip Vertical" });
6278
+ return this.transform(this.inFrame(frame2.angle, m), { ids: options.ids, label: axis === "horizontal" ? "Flip Horizontal" : "Flip Vertical" });
5148
6279
  }
5149
6280
  /**
5150
6281
  * Aligns the selected nodes to their combined bounds, or to the artboard when one node is
@@ -5271,7 +6402,7 @@ var VectorEditorController = class {
5271
6402
  const opacity = n.opacity;
5272
6403
  const m = nodeMatrix(n);
5273
6404
  out.push(
5274
- ...n.children.map((c) => n.transform ? transformVectorNode(c, m) : c).map((c) => opacity !== void 0 && opacity < 1 ? { ...c, opacity: (c.opacity ?? 1) * opacity } : c)
6405
+ ...n.children.map((c) => c.blendStep ? (({ blendStep: _s, locked: _l, ...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)
5275
6406
  );
5276
6407
  } else if (n.type === "group") {
5277
6408
  const children = flatten2(n.children);
@@ -5435,6 +6566,173 @@ var VectorEditorController = class {
5435
6566
  return nodes.map((n) => n.id);
5436
6567
  }
5437
6568
  /** The Shape Builder map of the selected (or given) paths, cached per document and selection. */
6569
+ // -------------------------------------------------------------------------
6570
+ // Blends
6571
+ // -------------------------------------------------------------------------
6572
+ /** Selected (or given) blend groups; a selected key or step counts as its blend. */
6573
+ blendTargets(ids) {
6574
+ const out = /* @__PURE__ */ new Map();
6575
+ for (const id of ids ?? this.selection) {
6576
+ const node = this.getNode(id);
6577
+ const group = isVectorBlendGroup(node) ? node : locateVectorNode(this.doc.nodes, id)?.parent;
6578
+ if (isVectorBlendGroup(group)) out.set(group.id, group);
6579
+ }
6580
+ return [...out.values()];
6581
+ }
6582
+ /**
6583
+ * Blends two or more selected (or given) paths or groups: they become the keys of a live
6584
+ * blend group (bottom to top, or in the order given with `keepOrder`) whose in-betweens are
6585
+ * regenerated whenever a key or the options change. The keys must share a parent. Returns
6586
+ * the blend group's id, or null.
6587
+ */
6588
+ blend(input = {}) {
6589
+ const ids = [...new Set(input.ids ?? this.selectedRoots())].filter((id2) => {
6590
+ const n = this.getNode(id2);
6591
+ return !!n && !n.blendStep && !n.locked;
6592
+ });
6593
+ if (ids.length < 2) return null;
6594
+ const locs = ids.map((id2) => locateVectorNode(this.doc.nodes, id2));
6595
+ const parent = locs[0].parent?.id ?? null;
6596
+ if (locs.some((l) => (l.parent?.id ?? null) !== parent)) return null;
6597
+ const order = input.keepOrder ? ids : ids.slice().sort((a, b) => locs[ids.indexOf(a)].index - locs[ids.indexOf(b)].index);
6598
+ const blend = blendOptionsOf({ ...this.blendDefaults, ...input });
6599
+ if (input.starts) {
6600
+ const starts = Object.fromEntries(Object.entries(input.starts).filter(([k, v2]) => order.includes(k) && Number.isInteger(v2) && v2 >= 0));
6601
+ if (Object.keys(starts).length) blend.starts = starts;
6602
+ }
6603
+ const id = this.newId();
6604
+ this.change("Blend", () => {
6605
+ const set = new Set(order);
6606
+ const regroup = (list) => {
6607
+ const members = order.map((m) => list.find((n) => n.id === m));
6608
+ const top = Math.max(...members.map((m) => list.indexOf(m)));
6609
+ const out = [];
6610
+ list.forEach((n, i) => {
6611
+ if (i === top) out.push({ id, type: "group", name: input.name ?? "Blend", blend, children: members });
6612
+ if (!set.has(n.id)) out.push(n);
6613
+ });
6614
+ return out;
6615
+ };
6616
+ if (parent === null) this.setDoc(regroup(this.doc.nodes));
6617
+ else this.setDoc(mapTree(this.doc.nodes, (n) => n.id === parent && n.type === "group" ? { ...n, children: regroup(n.children) } : n));
6618
+ this.selection = [id];
6619
+ this.anchors = [];
6620
+ this.editingId = null;
6621
+ });
6622
+ return id;
6623
+ }
6624
+ /** Sets the options new blends get (spacing, steps, distance, orientation, morph). */
6625
+ setBlendDefaults(patch) {
6626
+ this.blendDefaults = { ...this.blendDefaults, ...blendOptionsOf(patch) };
6627
+ this.notify();
6628
+ }
6629
+ /**
6630
+ * Adds a node as the last key of a blend (it must share the blend's parent); `start` is the
6631
+ * anchor of its first subpath the blend starts from. One undo step.
6632
+ */
6633
+ addBlendKey(blendId, nodeId, options = {}) {
6634
+ const group = this.getNode(blendId);
6635
+ const node = this.getNode(nodeId);
6636
+ if (!isVectorBlendGroup(group)) return { ok: false, message: `"${blendId}" is not a blend.` };
6637
+ if (!node || node.blendStep || node.locked || node.id === blendId) return { ok: false, message: `"${nodeId}" cannot be a key.` };
6638
+ const parentOf = (id) => locateVectorNode(this.doc.nodes, id)?.parent?.id ?? null;
6639
+ if (parentOf(blendId) !== parentOf(nodeId)) return { ok: false, message: "The new key must sit beside the blend (same parent)." };
6640
+ const start = options.start;
6641
+ this.change("Blend", () => {
6642
+ const blend = start !== null && start !== void 0 ? { ...group.blend, starts: { ...group.blend.starts, [nodeId]: start } } : group.blend;
6643
+ const m = invertMatrix(nodeMatrix(group));
6644
+ const key = group.transform ? transformVectorNode(node, m) : node;
6645
+ const without = mapTree(this.doc.nodes, (n) => n.id === nodeId ? null : n);
6646
+ this.setDoc(spliceTree(without, blendId, [{ ...group, blend, children: [...group.children, key] }]));
6647
+ this.selection = [blendId];
6648
+ this.anchors = [];
6649
+ });
6650
+ return { ok: true };
6651
+ }
6652
+ /** Changes the spacing, steps, distance, orientation or morph of the selected (or given) blends. */
6653
+ setBlendOptions(patch, options = {}) {
6654
+ const groups = this.blendTargets(options.ids);
6655
+ if (!groups.length) return { ok: false, message: "Select a blend." };
6656
+ const clean2 = blendOptionsOf(patch);
6657
+ if (!Object.keys(clean2).length) return { ok: false, message: "No valid blend options were given." };
6658
+ const set = new Set(groups.map((g) => g.id));
6659
+ this.change("Blend Options", () => {
6660
+ this.setDoc(mapTree(this.doc.nodes, (n) => set.has(n.id) && n.type === "group" && n.blend ? { ...n, blend: { ...n.blend, ...clean2 } } : n));
6661
+ });
6662
+ return { ok: true };
6663
+ }
6664
+ /** Expand: turns the selected (or given) blends into plain groups of their keys and steps. Returns their ids. */
6665
+ expandBlend(options = {}) {
6666
+ const groups = this.blendTargets(options.ids);
6667
+ if (!groups.length) return [];
6668
+ const set = new Set(groups.map((g) => g.id));
6669
+ const used = /* @__PURE__ */ new Set();
6670
+ walkVectorNodes(this.doc.nodes, (n) => used.add(n.id));
6671
+ const fresh = () => {
6672
+ let id = this.createId();
6673
+ while (used.has(id)) id = this.createId();
6674
+ used.add(id);
6675
+ return id;
6676
+ };
6677
+ this.change("Expand Blend", () => {
6678
+ this.setDoc(mapTree(this.doc.nodes, (n) => set.has(n.id) && n.type === "group" ? expandVectorBlend(n, fresh) : n));
6679
+ this.selection = [...set];
6680
+ this.anchors = [];
6681
+ });
6682
+ return [...set];
6683
+ }
6684
+ /** Release: replaces the selected (or given) blends with their keys (and a replaced spine as a path). */
6685
+ releaseBlend(options = {}) {
6686
+ const groups = this.blendTargets(options.ids);
6687
+ if (!groups.length) return [];
6688
+ const released = [];
6689
+ this.change("Release Blend", () => {
6690
+ for (const g of groups) {
6691
+ const nodes = releaseVectorBlend(g, () => this.newId());
6692
+ released.push(...nodes.map((n) => n.id));
6693
+ this.setDoc(spliceTree(this.doc.nodes, g.id, nodes));
6694
+ }
6695
+ this.selection = released;
6696
+ this.anchors = [];
6697
+ });
6698
+ return released;
6699
+ }
6700
+ /**
6701
+ * Replace Spine: the selected blend's steps follow the selected path instead of the straight
6702
+ * lines between its keys. The path's first subpath becomes the spine (the path is removed)
6703
+ * and the keys move onto it, spread evenly by arc length.
6704
+ */
6705
+ replaceBlendSpine(options = {}) {
6706
+ const roots = options.ids ?? this.selectedRoots();
6707
+ const groups = this.blendTargets(roots);
6708
+ const paths = roots.map((id) => this.getNode(id)).filter((n) => n?.type === "path" && !n.blendStep && !isVectorBlendGroup(locateVectorNode(this.doc.nodes, n.id)?.parent));
6709
+ if (groups.length !== 1 || paths.length !== 1) return { ok: false, message: "Select one blend and one path to be its spine." };
6710
+ const group = groups[0];
6711
+ const path = paths[0];
6712
+ const first = path.subpaths.find((sp) => sp.anchors.length >= 2);
6713
+ if (!first) return { ok: false, message: "The spine needs a path with at least two points." };
6714
+ const m = multiplyMatrices(this.worldOf(path.id), invertMatrix(this.worldOf(group.id)));
6715
+ const spine = transformVectorNode({ id: "", type: "path", subpaths: [first] }, m).subpaths[0];
6716
+ this.change("Replace Spine", () => {
6717
+ const withSpine = { ...group, blend: { ...group.blend, spine } };
6718
+ const targets = vectorBlendKeyTargets(withSpine);
6719
+ let k = 0;
6720
+ const children = withSpine.children.map((c) => {
6721
+ if (c.blendStep) return c;
6722
+ const target = targets[k++];
6723
+ const b = vectorNodeBounds(c);
6724
+ if (!target || !b) return c;
6725
+ const dx = target[0] - (b.x + b.width / 2);
6726
+ const dy = target[1] - (b.y + b.height / 2);
6727
+ return Math.hypot(dx, dy) > 1e-9 ? transformVectorNode(c, translateMatrix(dx, dy)) : c;
6728
+ });
6729
+ this.setDoc(mapTree(this.doc.nodes, (n) => n.id === path.id ? null : n.id === group.id ? { ...withSpine, children } : n));
6730
+ this.selection = [group.id];
6731
+ this.anchors = [];
6732
+ if (this.editingId === path.id) this.editingId = null;
6733
+ });
6734
+ return { ok: true };
6735
+ }
5438
6736
  shapeBuilderMap(ids) {
5439
6737
  const roots = ids ?? this.selectedRoots();
5440
6738
  const key = roots.join("\0");
@@ -6173,9 +7471,9 @@ var VectorEditorController = class {
6173
7471
  }
6174
7472
  hitTransformHandle(screen) {
6175
7473
  if (this.tool !== "select" || !this.selection.length) return null;
6176
- const frame = this.frameOf(this.selectedRoots());
6177
- if (!frame) return null;
6178
- const { box, handles } = getVectorSelectionBoxGeometry(frame, this.viewport);
7474
+ const frame2 = this.frameOf(this.selectedRoots());
7475
+ if (!frame2) return null;
7476
+ const { box, handles } = getVectorSelectionBoxGeometry(frame2, this.viewport);
6179
7477
  const r = this.hitRadius + 2;
6180
7478
  const usable2 = box.width < 2 * r || box.height < 2 * r ? handles.filter((h) => h.handle.length === 2 || h.handle === "rotate") : handles;
6181
7479
  for (const h of usable2) if (Math.hypot(h.screen.x - screen.x, h.screen.y - screen.y) <= r * Math.SQRT2) return h.handle;
@@ -6287,6 +7585,11 @@ var VectorEditorController = class {
6287
7585
  const seg = this.nearestOutline(p);
6288
7586
  return seg ? { nodeId: seg.nodeId, segment: seg } : null;
6289
7587
  }
7588
+ case "blend": {
7589
+ const hit = this.blendHit(screen, p);
7590
+ if (!hit) return null;
7591
+ return hit.anchor === null ? { nodeId: hit.nodeId } : { nodeId: hit.nodeId, anchor: { nodeId: hit.nodeId, subpath: 0, index: hit.anchor } };
7592
+ }
6290
7593
  case "width": {
6291
7594
  const hit = this.hitWidthHandle(screen);
6292
7595
  if (hit) return { nodeId: hit.ref.nodeId, widthPoint: hit.ref };
@@ -6298,13 +7601,19 @@ var VectorEditorController = class {
6298
7601
  }
6299
7602
  }
6300
7603
  widthView() {
6301
- return { document: this.doc, selection: this.selection, tool: this.tool, hover: this.hover, interaction: this.interaction, selectedWidthPoint: this.widthPoint };
7604
+ return { document: this.doc, selection: this.selection, tool: this.tool, hover: this.hover, interaction: this.interaction, selectedWidthPoints: this.widthPoints };
6302
7605
  }
6303
7606
  /** The width point centre or side end under `screen` (width tool). */
6304
7607
  hitWidthHandle(screen) {
7608
+ const shown = this.hitWidthHandleIn(screen, this.widthView());
7609
+ if (shown) return shown;
7610
+ const spot = this.strokeSpotAt(this.screenToDocument(screen));
7611
+ return spot && spot.nodeId !== this.hover?.nodeId ? this.hitWidthHandleIn(screen, { ...this.widthView(), hover: { nodeId: spot.nodeId } }) : null;
7612
+ }
7613
+ hitWidthHandleIn(screen, view) {
6305
7614
  let best = null;
6306
7615
  let bestD = Infinity;
6307
- for (const h of getVectorWidthHandles(this.widthView())) {
7616
+ for (const h of getVectorWidthHandles(view)) {
6308
7617
  for (const [part, q] of [["center", h], ["left", h.left], ["right", h.right]]) {
6309
7618
  const s = this.documentToScreen(q);
6310
7619
  const d = Math.hypot(s.x - screen.x, s.y - screen.y);
@@ -6357,11 +7666,10 @@ var VectorEditorController = class {
6357
7666
  const nodeId = hit?.ref.nodeId ?? spot?.nodeId;
6358
7667
  const node = nodeId ? this.getNode(nodeId) : void 0;
6359
7668
  const g = node?.type === "path" ? vectorStrokeWidthGeometry(node) : null;
7669
+ if (this.widthEdit) this.widthEdit = null;
6360
7670
  if (!node || node.type !== "path" || !g) {
6361
- if (this.widthPoint) {
6362
- this.widthPoint = null;
6363
- this.notify();
6364
- }
7671
+ if (!input.shift) this.widthPoints = [];
7672
+ this.notify();
6365
7673
  return;
6366
7674
  }
6367
7675
  const before = this.snapshot();
@@ -6370,6 +7678,8 @@ var VectorEditorController = class {
6370
7678
  let ref;
6371
7679
  let mode = "width";
6372
7680
  let side = "both";
7681
+ let group = [];
7682
+ let shift = false;
6373
7683
  let t;
6374
7684
  let subpath;
6375
7685
  if (hit) {
@@ -6377,8 +7687,20 @@ var VectorEditorController = class {
6377
7687
  ref = { nodeId: node.id, index: hit.ref.index, subpath: hit.ref.subpath };
6378
7688
  t = profile[ref.index]?.t ?? 0;
6379
7689
  subpath = hit.ref.subpath;
6380
- if (hit.ref.part === "center") mode = "move";
7690
+ if (hit.ref.part === "center") mode = input.alt && profile.length < VECTOR_WIDTH_PROFILE_MAX_POINTS ? "copy" : "move";
6381
7691
  else if (input.alt) side = hit.ref.part;
7692
+ const same = this.widthPoints.length > 0 && this.widthPoints[0].nodeId === node.id;
7693
+ const selected = same && this.widthPoints.some((r) => r.index === ref.index);
7694
+ if (input.shift && mode !== "copy") {
7695
+ if (selected) {
7696
+ this.widthPoints = this.widthPoints.filter((r) => r.index !== ref.index);
7697
+ this.notify();
7698
+ return;
7699
+ }
7700
+ this.widthPoints = same ? [...this.widthPoints, ref] : [ref];
7701
+ shift = true;
7702
+ } else if (mode === "copy" || !selected) this.widthPoints = [ref];
7703
+ if (mode !== "copy") group = this.widthPoints.filter((r) => r.index !== ref.index).map((r) => r.index);
6382
7704
  } else {
6383
7705
  const near2 = nearestStrokePosition(g.line, [local.x, local.y]);
6384
7706
  const inserted = insertWidthPoint(strokeWidthProfileOf(node), near2.t);
@@ -6387,17 +7709,21 @@ var VectorEditorController = class {
6387
7709
  t = near2.t;
6388
7710
  ref = { nodeId: node.id, index: inserted.index, subpath };
6389
7711
  if (input.alt) side = near2.side;
7712
+ this.widthPoints = [ref];
6390
7713
  }
6391
7714
  const at2 = strokePositionPoint(g.line, subpath, t);
6392
7715
  if (!at2) return;
6393
7716
  const base = { ...this.doc, nodes: mapTree(this.doc.nodes, (n) => n.id === node.id ? { ...n, strokeWidthProfile: profile } : n) };
6394
7717
  this.doc = base;
6395
- this.widthPoint = { nodeId: node.id, index: ref.index, subpath };
6396
7718
  this.drag = {
6397
7719
  kind: "width",
6398
7720
  ref,
6399
7721
  mode,
6400
7722
  side,
7723
+ source: ref.index,
7724
+ group,
7725
+ shift,
7726
+ renumber: null,
6401
7727
  origin: { x: at2.point[0], y: at2.point[1] },
6402
7728
  normal: { x: at2.tangent[1], y: -at2.tangent[0] },
6403
7729
  unit: g.unit,
@@ -6412,6 +7738,31 @@ var VectorEditorController = class {
6412
7738
  this.interaction = { type: "width", ref, mode, width: (w.left + w.right) * g.unit };
6413
7739
  this.notify(!hit);
6414
7740
  }
7741
+ /**
7742
+ * A width-tool slide to `t` on the dragged point's subpath lands exactly on another width
7743
+ * point (not `skip`) within the pick distance on screen: dropping a point onto another makes
7744
+ * a discontinuous point (two points at one `t`, the width steps there).
7745
+ */
7746
+ snapWidthT(drag, node, line2, t, skip) {
7747
+ const m = this.worldOf(node.id, drag.base);
7748
+ const at2 = strokePositionPoint(line2, drag.ref.subpath, t);
7749
+ if (!at2) return t;
7750
+ const here = this.documentToScreen(applyMatrix(m, { x: at2.point[0], y: at2.point[1] }));
7751
+ let best = t;
7752
+ let bestD = this.hitRadius;
7753
+ drag.profile.forEach((w, i) => {
7754
+ if (i === skip || drag.group.includes(i)) return;
7755
+ const q = strokePositionPoint(line2, drag.ref.subpath, w.t);
7756
+ if (!q) return;
7757
+ const s = this.documentToScreen(applyMatrix(m, { x: q.point[0], y: q.point[1] }));
7758
+ const d = Math.hypot(s.x - here.x, s.y - here.y);
7759
+ if (d <= bestD) {
7760
+ best = w.t;
7761
+ bestD = d;
7762
+ }
7763
+ });
7764
+ return best;
7765
+ }
6415
7766
  /** The nearest outline point of any visible, unlocked path within the pick distance of document point `p`. */
6416
7767
  nearestOutline(p) {
6417
7768
  let best = null;
@@ -6465,6 +7816,8 @@ var VectorEditorController = class {
6465
7816
  case "shapeBuilder":
6466
7817
  case "paintBucket":
6467
7818
  return this.downBuilder(p, input);
7819
+ case "blend":
7820
+ return this.downBlend(screen, p);
6468
7821
  case "scissors": {
6469
7822
  const seg = this.nearestOutline(p);
6470
7823
  if (seg) this.cutPathAt(seg.nodeId, seg.point);
@@ -6480,13 +7833,53 @@ var VectorEditorController = class {
6480
7833
  return this.downShape(p, input);
6481
7834
  }
6482
7835
  }
7836
+ /** The shape under the pointer for the Blend tool, and the anchor of its first subpath under it. */
7837
+ blendHit(screen, p) {
7838
+ const node = hitTestVectorNodes(this.doc.nodes, p, this.tolerance());
7839
+ if (!node) return null;
7840
+ let anchor = null;
7841
+ if (node.type === "path" && node.subpaths[0]) {
7842
+ const refs = node.subpaths[0].anchors.map((_, index) => ({ nodeId: node.id, subpath: 0, index }));
7843
+ anchor = this.hitAnchor(screen, refs)?.index ?? null;
7844
+ }
7845
+ return { nodeId: node.id, anchor };
7846
+ }
7847
+ /** Blend tool: the first click picks the shape to blend from, the next one blends it into the shape clicked. */
7848
+ downBlend(screen, p) {
7849
+ const hit = this.blendHit(screen, p);
7850
+ const start = this.blendStart;
7851
+ if (!hit) {
7852
+ if (start) {
7853
+ this.blendStart = null;
7854
+ this.notify();
7855
+ }
7856
+ return;
7857
+ }
7858
+ if (!start || start.nodeId === hit.nodeId) {
7859
+ this.blendStart = hit;
7860
+ this.selection = [hit.nodeId];
7861
+ this.anchors = [];
7862
+ this.notify();
7863
+ return;
7864
+ }
7865
+ this.blendStart = null;
7866
+ const first = this.getNode(start.nodeId);
7867
+ if (isVectorBlendGroup(first)) {
7868
+ if (!this.addBlendKey(first.id, hit.nodeId, { start: hit.anchor }).ok) this.notify();
7869
+ return;
7870
+ }
7871
+ const starts = {};
7872
+ if (start.anchor !== null) starts[start.nodeId] = start.anchor;
7873
+ if (hit.anchor !== null) starts[hit.nodeId] = hit.anchor;
7874
+ if (!this.blend({ ids: [start.nodeId, hit.nodeId], keepOrder: true, starts })) this.notify();
7875
+ }
6483
7876
  downSelect(screen, p, input) {
6484
7877
  const handle = this.hitTransformHandle(screen);
6485
7878
  if (handle) {
6486
- const frame = this.frameOf(this.selectedRoots());
7879
+ const frame2 = this.frameOf(this.selectedRoots());
6487
7880
  const before = this.snapshot();
6488
- const center = applyMatrix(rotateMatrix(frame.angle), { x: frame.x + frame.width / 2, y: frame.y + frame.height / 2 });
6489
- this.drag = handle === "rotate" ? { kind: "rotate", start: p, center, before, base: this.doc } : { kind: "scale", handle, start: p, frame, before, base: this.doc };
7881
+ const center = applyMatrix(rotateMatrix(frame2.angle), { x: frame2.x + frame2.width / 2, y: frame2.y + frame2.height / 2 });
7882
+ this.drag = handle === "rotate" ? { kind: "rotate", start: p, center, before, base: this.doc } : { kind: "scale", handle, start: p, frame: frame2, before, base: this.doc };
6490
7883
  this.interaction = handle === "rotate" ? { type: "rotate", angle: 0 } : { type: "scale", handle, sx: 1, sy: 1 };
6491
7884
  this.notify();
6492
7885
  return;
@@ -6896,19 +8289,35 @@ var VectorEditorController = class {
6896
8289
  drag.moved = true;
6897
8290
  const node = findVectorNode(drag.base.nodes, drag.ref.nodeId);
6898
8291
  const local = this.toLocal(node.id, p, drag.base);
8292
+ const src = drag.profile[drag.source];
6899
8293
  let profile;
6900
- if (drag.mode === "move") {
8294
+ let index = drag.source;
8295
+ if (drag.mode !== "width") {
6901
8296
  const g = vectorStrokeWidthGeometry(node);
6902
8297
  const near2 = g ? nearestStrokePosition(g.line, [local.x, local.y], drag.ref.subpath) : null;
6903
- if (!near2) return;
6904
- profile = setWidthPoint(drag.profile, drag.ref.index, { t: near2.t });
8298
+ if (!g || !near2) return;
8299
+ const t = drag.group.length ? near2.t : this.snapWidthT(drag, node, g.line, near2.t, drag.source);
8300
+ if (drag.mode === "copy") {
8301
+ ({ points: profile, index } = insertWidthPoint(drag.profile, t, { left: src.left, right: src.right }, { step: true }));
8302
+ } else if (drag.group.length) {
8303
+ const shifted = shiftWidthPoints(drag.profile, [drag.source, ...drag.group], t - src.t);
8304
+ profile = shifted.points;
8305
+ drag.renumber = shifted.renumber;
8306
+ index = shifted.renumber.get(drag.source) ?? drag.source;
8307
+ } else profile = setWidthPoint(drag.profile, drag.source, { t });
6905
8308
  } else {
6906
8309
  const off = ((local.x - drag.origin.x) * drag.normal.x + (local.y - drag.origin.y) * drag.normal.y) / drag.unit;
6907
8310
  const patch = drag.side === "both" ? { left: Math.abs(off), right: Math.abs(off) } : drag.side === "left" ? { left: Math.max(0, off) } : { right: Math.max(0, -off) };
6908
- profile = setWidthPoint(drag.profile, drag.ref.index, patch);
8311
+ profile = setWidthPoint(drag.profile, drag.source, patch);
8312
+ if (drag.group.length) {
8313
+ const dl = profile[drag.source].left - src.left;
8314
+ const dr = profile[drag.source].right - src.right;
8315
+ for (const i of drag.group) profile = setWidthPoint(profile, i, { left: Math.max(0, drag.profile[i].left + dl), right: Math.max(0, drag.profile[i].right + dr) });
8316
+ }
6909
8317
  }
6910
8318
  this.doc = { ...drag.base, nodes: mapTree(drag.base.nodes, (n) => n.id === node.id ? { ...n, strokeWidthProfile: profile } : n) };
6911
- const w = profile[drag.ref.index];
8319
+ drag.ref = { ...drag.ref, index };
8320
+ const w = profile[index];
6912
8321
  this.interaction = { type: "width", ref: drag.ref, mode: drag.mode, width: (w.left + w.right) * drag.unit };
6913
8322
  this.notify(true);
6914
8323
  return;
@@ -7054,12 +8463,23 @@ var VectorEditorController = class {
7054
8463
  return;
7055
8464
  }
7056
8465
  case "width": {
7057
- if (drag.moved) return this.commit(drag.mode === "move" ? "Move Width Point" : "Width Point", drag.before);
8466
+ if (drag.moved) {
8467
+ const several = drag.group.length > 0;
8468
+ if (drag.mode === "copy") this.widthPoints = [drag.ref];
8469
+ else if (drag.mode === "move" && several) {
8470
+ this.widthPoints = [drag.source, ...drag.group].map((i) => ({ ...drag.ref, index: drag.renumber?.get(i) ?? i }));
8471
+ } else if (!several) this.widthPoints = [drag.ref];
8472
+ const label = drag.mode === "copy" ? "Copy Width Point" : drag.mode === "move" ? several ? "Move Width Points" : "Move Width Point" : several ? "Width Points" : "Width Point";
8473
+ return this.commit(label, drag.before);
8474
+ }
7058
8475
  if (drag.created) {
7059
8476
  this.doc = drag.before.doc;
7060
- this.widthPoint = null;
8477
+ this.widthPoints = [];
7061
8478
  this.notify(true);
7062
- } else this.notify();
8479
+ } else {
8480
+ if (!drag.shift && drag.group.length) this.widthPoints = [drag.ref];
8481
+ this.notify();
8482
+ }
7063
8483
  return;
7064
8484
  }
7065
8485
  case "builder":
@@ -7148,7 +8568,10 @@ var VectorEditorController = class {
7148
8568
  }
7149
8569
  this.restore(drag.before);
7150
8570
  }
7151
- /** Double click: select tool enters path editing, direct tool toggles an anchor or adds one. */
8571
+ /**
8572
+ * Double click: select tool enters path editing, direct tool toggles an anchor or adds one,
8573
+ * width tool opens the width-point editor (`widthPointEdit`).
8574
+ */
7152
8575
  doubleClick(screen) {
7153
8576
  const p = this.screenToDocument(screen);
7154
8577
  if (this.tool === "select") {
@@ -7176,6 +8599,16 @@ var VectorEditorController = class {
7176
8599
  }
7177
8600
  return;
7178
8601
  }
8602
+ if (this.tool === "width") {
8603
+ const hit = this.hitWidthHandle(screen);
8604
+ if (hit) {
8605
+ this.openWidthPointEdit(hit.ref);
8606
+ return;
8607
+ }
8608
+ const spot = this.strokeSpotAt(p);
8609
+ if (spot) this.openWidthPointEdit({ nodeId: spot.nodeId, subpath: spot.subpath, t: spot.t });
8610
+ return;
8611
+ }
7179
8612
  if (this.tool === "pen") this.finishPath();
7180
8613
  }
7181
8614
  // -------------------------------------------------------------------------
@@ -7236,11 +8669,15 @@ var VectorEditorController = class {
7236
8669
  switch (key) {
7237
8670
  case "Escape":
7238
8671
  if (this.drag) this.cancelDrag();
7239
- else if (this.tool === "width" && this.widthPoint) {
7240
- this.widthPoint = null;
8672
+ else if (this.widthEdit) this.closeWidthPointEdit();
8673
+ else if (this.tool === "width" && this.widthPoints.length) {
8674
+ this.widthPoints = [];
7241
8675
  this.notify();
7242
8676
  } else if (this.pen) this.finishPath();
7243
- else if (this.tool === "direct" && this.anchors.length) {
8677
+ else if (this.blendStart) {
8678
+ this.blendStart = null;
8679
+ this.notify();
8680
+ } else if (this.tool === "direct" && this.anchors.length) {
7244
8681
  this.anchors = [];
7245
8682
  this.notify();
7246
8683
  } else if (this.tool !== "select") this.setTool("select");
@@ -7260,7 +8697,7 @@ var VectorEditorController = class {
7260
8697
  return false;
7261
8698
  case "Delete":
7262
8699
  case "Backspace":
7263
- if (this.tool === "width" && this.widthPoint) return this.removeWidthPoint(this.widthPoint).ok;
8700
+ if (this.tool === "width" && this.widthPoints.length) return this.removeWidthPoints().ok;
7264
8701
  if (this.pen) {
7265
8702
  const node = this.getNode(this.pen.nodeId);
7266
8703
  const sp = node.subpaths[this.pen.subpath];
@@ -7545,7 +8982,7 @@ function getVectorOverlayRenderData(state) {
7545
8982
  });
7546
8983
  const spot = state.tool === "width" && !it ? state.hover?.strokeSpot : null;
7547
8984
  const widthSpot = spot ? { ...sc(spot.point), left: sc(spot.left), right: sc(spot.right) } : null;
7548
- if (it?.type === "width") status = it.mode === "move" ? "Move width point" : `W ${fmt(it.width)}`;
8985
+ if (it?.type === "width") status = it.mode === "move" ? "Move width point" : it.mode === "copy" ? "Copy width point" : `W ${fmt(it.width)}`;
7549
8986
  let builder = null;
7550
8987
  if (state.builder) {
7551
8988
  const v2 = state.viewport;
@@ -7566,8 +9003,79 @@ function getVectorOverlayRenderData(state) {
7566
9003
  status = state.builder.mode === "erase" ? `Delete ${n} ${n === 1 ? "face" : "faces"}` : `Merge ${n} ${n === 1 ? "face" : "faces"}`;
7567
9004
  }
7568
9005
  }
7569
- return { outlines, selectionBox, anchors, handles, marquee, penPreview, artboard, guides, status, toolStroke, pencilString, cutPoint, cornerWidgets, widthPoints, widthSpot, builder };
9006
+ const blend = blendOverlay(state);
9007
+ if (state.tool === "blend" && !it) status = state.blendStart ? "Click another shape to blend into" : "Click a shape to blend from";
9008
+ return { outlines, selectionBox, anchors, handles, marquee, penPreview, artboard, guides, status, toolStroke, pencilString, cutPoint, cornerWidgets, widthPoints, widthSpot, builder, blend };
9009
+ }
9010
+ function selectedBlends(state) {
9011
+ const out = /* @__PURE__ */ new Map();
9012
+ for (const id of state.selection) {
9013
+ const node = findVectorNode(state.document.nodes, id);
9014
+ const group = isVectorBlendGroup(node) ? node : locateVectorNode(state.document.nodes, id)?.parent;
9015
+ if (isVectorBlendGroup(group)) out.set(group.id, group);
9016
+ }
9017
+ return [...out.values()];
7570
9018
  }
9019
+ function blendOverlay(state) {
9020
+ const v2 = state.viewport;
9021
+ const view = [v2.zoom, 0, 0, v2.zoom, v2.x, v2.y];
9022
+ const doc = state.document;
9023
+ const sc = (p, m) => {
9024
+ const q = { x: m[0] * p.x + m[2] * p.y + m[4], y: m[1] * p.x + m[3] * p.y + m[5] };
9025
+ return { x: fmt(q.x), y: fmt(q.y) };
9026
+ };
9027
+ const spines = selectedBlends(state).flatMap((g) => {
9028
+ const s = vectorBlendSpine(g);
9029
+ if (!s) return [];
9030
+ const m = multiplyMatrices(vectorWorldMatrix(doc.nodes, g.id) ?? IDENTITY_MATRIX, view);
9031
+ const d = bezierPathToSvg(transformBezierPath(subpathsToBezier([s.spine], { corners: false }), m), { precision: 2 });
9032
+ const keys = s.stops.map((stop) => {
9033
+ const p = vectorBlendRailAt(s.rail, stop).point;
9034
+ return sc({ x: p[0], y: p[1] }, m);
9035
+ });
9036
+ return [{ id: g.id, d, keys, replaced: s.replaced }];
9037
+ });
9038
+ let start = null;
9039
+ const anchors = [];
9040
+ if (state.tool === "blend") {
9041
+ const from = state.blendStart ? findVectorNode(doc.nodes, state.blendStart.nodeId) : void 0;
9042
+ if (from && state.blendStart) {
9043
+ const a = state.blendStart.anchor === null ? void 0 : getAnchorWorld(doc, { nodeId: from.id, subpath: 0, index: state.blendStart.anchor });
9044
+ start = { d: screenOutline(state, from), anchor: a ? sc(a, view) : null };
9045
+ }
9046
+ const hovered = state.hover?.nodeId ? findVectorNode(doc.nodes, state.hover.nodeId) : void 0;
9047
+ if (hovered?.type === "path" && hovered.subpaths[0]) {
9048
+ hovered.subpaths[0].anchors.forEach((_, index) => {
9049
+ const a = getAnchorWorld(doc, { nodeId: hovered.id, subpath: 0, index });
9050
+ if (a) anchors.push({ ...sc(a, view), hovered: state.hover?.anchor?.index === index });
9051
+ });
9052
+ }
9053
+ }
9054
+ return { spines, start, anchors };
9055
+ }
9056
+ function getVectorBlendControls(state) {
9057
+ const blends = selectedBlends(state);
9058
+ const doc = state.document;
9059
+ const roots = state.selection.filter((id) => {
9060
+ const node = findVectorNode(doc.nodes, id);
9061
+ return !!node && !node.blendStep && !node.locked && !state.selection.includes(locateVectorNode(doc.nodes, id)?.parent?.id ?? "");
9062
+ });
9063
+ const parents = new Set(roots.map((id) => locateVectorNode(doc.nodes, id)?.parent?.id ?? null));
9064
+ const paths = roots.filter((id) => findVectorNode(doc.nodes, id)?.type === "path" && !isVectorBlendGroup(locateVectorNode(doc.nodes, id)?.parent));
9065
+ const first = blends[0];
9066
+ return {
9067
+ ids: blends.map((g) => g.id),
9068
+ options: resolveVectorBlendOptions(first ? first.blend : state.blendDefaults),
9069
+ canBlend: roots.length >= 2 && parents.size === 1,
9070
+ canReplaceSpine: blends.length === 1 && paths.length === 1,
9071
+ hasSpine: !!first?.blend?.spine
9072
+ };
9073
+ }
9074
+ function applyVectorBlendOptions(controller, patch) {
9075
+ controller.setBlendDefaults(patch);
9076
+ if (getVectorBlendControls(controller.getState()).ids.length) controller.setBlendOptions(patch);
9077
+ }
9078
+ var VECTOR_BLEND_SPACING_LABELS = { steps: "Specified Steps", distance: "Specified Distance", smoothColor: "Smooth Color" };
7571
9079
  var VECTOR_OVERLAY_THEME = {
7572
9080
  accent: "#0ea5e9",
7573
9081
  /** Smart guide lines and labels. */
@@ -7752,6 +9260,18 @@ var WIDTH_POINTS = {
7752
9260
  },
7753
9261
  description: "Variable-width profile points, interpolated smoothly (monotone cubic); two points at one `t` make a step. `null` or `[]` strokes at a uniform width."
7754
9262
  };
9263
+ var BLEND_PROPS = {
9264
+ spacing: { type: "string", enum: ["steps", "distance", "smoothColor"], description: "How in-betweens are spaced (default steps)." },
9265
+ steps: { type: "integer", minimum: 1, maximum: 1e3, description: "In-betweens between each pair of keys (steps spacing; default 5)." },
9266
+ distance: { type: "number", exclusiveMinimum: 0, description: "Distance between in-betweens along the spine (distance spacing; default 20)." },
9267
+ orientToSpine: { type: "boolean", description: "Turn the in-betweens with a replaced spine." },
9268
+ linear: { type: "boolean", description: "Move anchors in straight lines instead of the rotation-aware morph." }
9269
+ };
9270
+ var checkBlendOptions = (params) => {
9271
+ if (params?.spacing !== void 0 && !VECTOR_BLEND_SPACINGS.includes(params.spacing)) fail(`Unknown blend spacing "${params.spacing}".`);
9272
+ if (params?.steps !== void 0 && !(Number.isFinite(params.steps) && params.steps >= 1)) fail("steps must be at least 1.");
9273
+ if (params?.distance !== void 0 && !(Number.isFinite(params.distance) && params.distance > 0)) fail("distance must be positive.");
9274
+ };
7755
9275
  var PAINT = {
7756
9276
  description: 'A CSS colour, `null` for none, or a gradient `{ type: "linear", angle, stops: [{ offset, color }] }`.'
7757
9277
  };
@@ -7816,7 +9336,9 @@ function createVectorCommands(controller, options = {}) {
7816
9336
  visible: n.visible !== false,
7817
9337
  locked: !!n.locked,
7818
9338
  selected: state.selection.includes(n.id),
7819
- ...n.type === "path" ? { fill: n.fill ?? null, stroke: n.stroke ?? null, strokeWidth: n.strokeWidth ?? 1, d: vectorPathData(n, 2) } : { children: n.children.length }
9339
+ ...n.type === "path" ? { fill: n.fill ?? null, stroke: n.stroke ?? null, strokeWidth: n.strokeWidth ?? 1, d: vectorPathData(n, 2) } : { children: n.children.length },
9340
+ ...n.type === "group" && n.blend ? { blend: { ...resolveVectorBlendOptions(n.blend), spine: !!n.blend.spine, keys: vectorBlendKeys(n).map((k) => k.id) } } : {},
9341
+ ...n.blendStep ? { blendStep: true } : {}
7820
9342
  });
7821
9343
  if (n.type === "group") visit(n.children, depth + 1);
7822
9344
  }
@@ -7964,6 +9486,31 @@ function createVectorCommands(controller, options = {}) {
7964
9486
  return { ok: true, points: node?.type === "path" ? node.strokeWidthProfile ?? [] : [] };
7965
9487
  }
7966
9488
  },
9489
+ {
9490
+ ...base,
9491
+ id: `${prefix}setWidthPoint`,
9492
+ label: "Width Point\u2026",
9493
+ keywords: ["width point", "variable width", "stroke width at", "taper", "profile"],
9494
+ description: "Sets one width point of a path's stroke profile numerically: `index` names an existing point (see `vector.setWidthProfile`'s result), leave it out to add one at `t`. `left` / `right` are side widths in document units (how far the stroke reaches on each side), `t` the position along each subpath (0\u20131); a point moved onto another's `t` makes a step. Returns the profile.",
9495
+ params: {
9496
+ type: "object",
9497
+ properties: {
9498
+ id: { type: "string", description: "The path." },
9499
+ index: { type: "integer", minimum: 0 },
9500
+ t: { type: "number", minimum: 0, maximum: 1 },
9501
+ left: { type: "number", minimum: 0 },
9502
+ right: { type: "number", minimum: 0 }
9503
+ },
9504
+ required: ["id"]
9505
+ },
9506
+ undoable: true,
9507
+ run: (_ctx, params) => {
9508
+ if (!params?.id) fail("Pass the path `id`.");
9509
+ check(controller.setWidthPoint({ nodeId: params.id, index: params.index ?? null }, { t: params.t, left: params.left, right: params.right }));
9510
+ const node = controller.getNode(params.id);
9511
+ return { ok: true, points: node?.type === "path" ? node.strokeWidthProfile ?? [] : [] };
9512
+ }
9513
+ },
7967
9514
  {
7968
9515
  ...base,
7969
9516
  id: `${prefix}outlineStroke`,
@@ -8036,6 +9583,86 @@ function createVectorCommands(controller, options = {}) {
8036
9583
  return { id };
8037
9584
  }
8038
9585
  },
9586
+ {
9587
+ ...base,
9588
+ id: `${prefix}blend`,
9589
+ label: "Make Blend\u2026",
9590
+ keywords: ["blend", "morph", "tween", "interpolate", "in-between", "steps", "transition"],
9591
+ 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.",
9592
+ params: {
9593
+ type: "object",
9594
+ properties: {
9595
+ ids: IDS,
9596
+ ...BLEND_PROPS,
9597
+ keepOrder: { type: "boolean", description: "Blend in the order of `ids` instead of stacking order." },
9598
+ starts: { type: "object", additionalProperties: { type: "integer", minimum: 0 }, description: "Start anchor per key id." }
9599
+ }
9600
+ },
9601
+ undoable: true,
9602
+ run: (_ctx, params) => {
9603
+ checkBlendOptions(params);
9604
+ const id = controller.blend({ ...params, ids: ids(params) });
9605
+ if (!id) fail("Select two or more shapes that share a parent.");
9606
+ return { id };
9607
+ }
9608
+ },
9609
+ {
9610
+ ...base,
9611
+ id: `${prefix}blendOptions`,
9612
+ label: "Blend Options\u2026",
9613
+ keywords: ["blend", "steps", "spacing", "distance", "smooth color", "orientation", "align to path"],
9614
+ 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.",
9615
+ params: { type: "object", properties: { ids: IDS, ...BLEND_PROPS } },
9616
+ undoable: true,
9617
+ run: (_ctx, params) => {
9618
+ checkBlendOptions(params);
9619
+ const r = controller.setBlendOptions(params ?? {}, { ids: params?.ids });
9620
+ if (!r.ok) fail(r.message);
9621
+ return { ok: true };
9622
+ }
9623
+ },
9624
+ {
9625
+ ...base,
9626
+ id: `${prefix}expandBlend`,
9627
+ label: "Expand Blend",
9628
+ keywords: ["blend", "expand", "flatten", "bake"],
9629
+ 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.",
9630
+ params: { type: "object", properties: { ids: IDS } },
9631
+ undoable: true,
9632
+ run: (_ctx, params) => {
9633
+ const groups = controller.expandBlend({ ids: params?.ids });
9634
+ if (!groups.length) fail("Select a blend to expand.");
9635
+ return { ids: groups };
9636
+ }
9637
+ },
9638
+ {
9639
+ ...base,
9640
+ id: `${prefix}releaseBlend`,
9641
+ label: "Release Blend",
9642
+ keywords: ["blend", "release", "undo blend"],
9643
+ 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.",
9644
+ params: { type: "object", properties: { ids: IDS } },
9645
+ undoable: true,
9646
+ run: (_ctx, params) => {
9647
+ const released = controller.releaseBlend({ ids: params?.ids });
9648
+ if (!released.length) fail("Select a blend to release.");
9649
+ return { ids: released };
9650
+ }
9651
+ },
9652
+ {
9653
+ ...base,
9654
+ id: `${prefix}replaceSpine`,
9655
+ label: "Replace Spine",
9656
+ keywords: ["blend", "spine", "path", "follow", "along path"],
9657
+ 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.",
9658
+ params: { type: "object", properties: { ids: IDS } },
9659
+ undoable: true,
9660
+ run: (_ctx, params) => {
9661
+ const r = controller.replaceBlendSpine({ ids: params?.ids });
9662
+ if (!r.ok) fail(r.message);
9663
+ return { ok: true };
9664
+ }
9665
+ },
8039
9666
  {
8040
9667
  ...base,
8041
9668
  id: `${prefix}pathfinder`,
@@ -8383,6 +10010,8 @@ function createVectorCommands(controller, options = {}) {
8383
10010
  ];
8384
10011
  }
8385
10012
  export {
10013
+ DEFAULT_VECTOR_BLEND_DISTANCE,
10014
+ DEFAULT_VECTOR_BLEND_STEPS,
8386
10015
  DEFAULT_VECTOR_PATH_EDIT,
8387
10016
  DEFAULT_VECTOR_SNAP,
8388
10017
  IDENTITY_MATRIX,
@@ -8390,6 +10019,10 @@ export {
8390
10019
  LazyMouse,
8391
10020
  VECTOR_ARROWHEADS,
8392
10021
  VECTOR_ARROWHEAD_LABELS,
10022
+ VECTOR_BLEND_MAX_SMOOTH_STEPS,
10023
+ VECTOR_BLEND_MAX_STEPS,
10024
+ VECTOR_BLEND_SPACINGS,
10025
+ VECTOR_BLEND_SPACING_LABELS,
8393
10026
  VECTOR_BUILDER_HATCH_GAP,
8394
10027
  VECTOR_CLIPBOARD_TYPE,
8395
10028
  VECTOR_CONSTRUCTION_ANGLES,
@@ -8423,6 +10056,7 @@ export {
8423
10056
  addBuilderHit,
8424
10057
  applyMatrix,
8425
10058
  applyMatrixLinear,
10059
+ applyVectorBlendOptions,
8426
10060
  applyVectorCornersAttribute,
8427
10061
  arrowheadInset,
8428
10062
  arrowheadPath,
@@ -8441,7 +10075,9 @@ export {
8441
10075
  cornerBisectorPoint,
8442
10076
  cornerRadiusChange,
8443
10077
  cornerSetback,
10078
+ correspondMorphContours,
8444
10079
  createVectorCommands,
10080
+ createVectorMorph,
8445
10081
  curvatureMoveAnchor,
8446
10082
  cutSubpathAt,
8447
10083
  dashIntervals,
@@ -8451,15 +10087,19 @@ export {
8451
10087
  eraseFromFill,
8452
10088
  eraseFromOutline,
8453
10089
  eraserRegion,
10090
+ evaluateVectorPathKeyframes,
10091
+ expandVectorBlend,
8454
10092
  expandVectorCorners,
8455
10093
  expandVectorCornersOf,
8456
10094
  findVectorNode,
8457
10095
  fitCornerRadius,
10096
+ generateVectorBlendChildren,
8458
10097
  generateWidthProfile,
8459
10098
  getAnchor,
8460
10099
  getAnchorWorld,
8461
10100
  getEditablePathIds,
8462
10101
  getTransformHandlePositions,
10102
+ getVectorBlendControls,
8463
10103
  getVectorCornerWidgets,
8464
10104
  getVectorOverlayRenderData,
8465
10105
  getVectorSceneRenderData,
@@ -8478,10 +10118,18 @@ export {
8478
10118
  inferAnchorType,
8479
10119
  insertAnchor,
8480
10120
  insertWidthPoint,
10121
+ interpolateMorphContours,
10122
+ interpolateRigidMatrix,
10123
+ interpolateVectorColor,
10124
+ interpolateVectorPaint,
10125
+ interpolateVectorPaths,
10126
+ interpolateVectorStyle,
10127
+ interpolateWidthProfiles,
8481
10128
  invertMatrix,
8482
10129
  isClosedBezierShape,
8483
10130
  isIdentityMatrix,
8484
10131
  isUniformWidthProfile,
10132
+ isVectorBlendGroup,
8485
10133
  isVectorTextTarget,
8486
10134
  joinSubpaths,
8487
10135
  knifePath,
@@ -8489,6 +10137,8 @@ export {
8489
10137
  locateVectorNode,
8490
10138
  matrixRotation,
8491
10139
  mirrorWidthProfile,
10140
+ morphContourToSubpath,
10141
+ morphPathsCross,
8492
10142
  moveAnchorHandle,
8493
10143
  multiplyMatrices,
8494
10144
  nearestSegment,
@@ -8503,11 +10153,17 @@ export {
8503
10153
  pressureWidthProfile,
8504
10154
  recognizeInkShape,
8505
10155
  rectSubpath,
10156
+ refineMorphContour,
10157
+ refreshVectorBlends,
10158
+ releaseVectorBlend,
8506
10159
  removeAnchorKeepingShape,
8507
10160
  removeAnchors,
8508
10161
  removeAnchorsKeepingShape,
8509
10162
  removeRedundantPoints,
8510
10163
  removeWidthPoints,
10164
+ resampleGradientStops,
10165
+ resolveVectorBlendOptions,
10166
+ reverseMorphContour,
8511
10167
  reverseSubpath,
8512
10168
  reverseWidthProfile,
8513
10169
  rotateMatrix,
@@ -8530,6 +10186,7 @@ export {
8530
10186
  subpathPointAt,
8531
10187
  subpathSegment,
8532
10188
  subpathSegmentCount,
10189
+ subpathToMorphContour,
8533
10190
  subpathsToBezier,
8534
10191
  taperWaveFactor,
8535
10192
  toVectorKeyInput,
@@ -8538,9 +10195,17 @@ export {
8538
10195
  transformVectorNode,
8539
10196
  translateAnchor,
8540
10197
  translateMatrix,
10198
+ turnMorphContour,
8541
10199
  unionBounds,
8542
10200
  variableStrokeOutline,
8543
10201
  variableStrokeRings,
10202
+ vectorBlendColorDistance,
10203
+ vectorBlendKeyTargets,
10204
+ vectorBlendKeys,
10205
+ vectorBlendRail,
10206
+ vectorBlendRailAt,
10207
+ vectorBlendSpine,
10208
+ vectorBlendStepCount,
8544
10209
  vectorBuilderErasedEdges,
8545
10210
  vectorCornerAt,
8546
10211
  vectorCorners,