@pixerate/editor 0.40.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"];
@@ -1327,9 +1338,25 @@ function decomposeRigid(m) {
1327
1338
  residual: [cos * a + sin * b, -sin * a + cos * b, cos * c + sin * d, -sin * c + cos * d, 0, 0]
1328
1339
  };
1329
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
+ }
1330
1350
  function bakeIntoGeometry(node, m) {
1331
1351
  if (isIdentityMatrix(m)) return node;
1332
- 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
+ }
1333
1360
  const path = node;
1334
1361
  const mirrored = m[0] * m[3] - m[1] * m[2] < 0 && path.strokeWidthProfile?.length;
1335
1362
  return {
@@ -1337,15 +1364,7 @@ function bakeIntoGeometry(node, m) {
1337
1364
  ...mirrored ? { strokeWidthProfile: mirrorWidthProfile(path.strokeWidthProfile) } : {},
1338
1365
  subpaths: path.subpaths.map((sp) => ({
1339
1366
  closed: sp.closed,
1340
- anchors: sp.anchors.map((a) => {
1341
- const p = applyMatrix(m, a);
1342
- const next = { x: p.x, y: p.y };
1343
- if (a.in) next.in = applyMatrix(m, a.in);
1344
- if (a.out) next.out = applyMatrix(m, a.out);
1345
- if (a.type) next.type = a.type;
1346
- if (a.corner) next.corner = { ...a.corner };
1347
- return next;
1348
- })
1367
+ anchors: sp.anchors.map((a) => mapAnchor(a, m))
1349
1368
  }))
1350
1369
  };
1351
1370
  }
@@ -1613,6 +1632,33 @@ function gradientMarkup(def) {
1613
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("");
1614
1633
  return `<${tag} id="${escapeAttr(def.id)}"${attrs}>${stops}</${tag}>`;
1615
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
+ }
1616
1662
  function vectorDocumentToSvg(doc, options = {}) {
1617
1663
  const precision = options.precision ?? 3;
1618
1664
  const prefix = options.idPrefix ?? "vg-";
@@ -1626,6 +1672,8 @@ function vectorDocumentToSvg(doc, options = {}) {
1626
1672
  if (node.transform) out2 += ` transform="matrix(${node.transform.map((n) => formatNumber(n, 6)).join(" ")})"`;
1627
1673
  if (node.opacity !== void 0 && node.opacity < 1) out2 += ` opacity="${num(node.opacity)}"`;
1628
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))}"`;
1629
1677
  return out2;
1630
1678
  };
1631
1679
  const emit = (node, depth) => {
@@ -1995,6 +2043,7 @@ function parseVectorSvg(svg, options = {}) {
1995
2043
  ...opacity !== void 0 && Number.isFinite(opacity) && opacity < 1 ? { opacity } : {},
1996
2044
  ...hidden ? { visible: false } : {}
1997
2045
  };
2046
+ if (child.attrs["data-vector-blend-step"] !== void 0) continue;
1998
2047
  if (child.name === "g" && child.attrs["data-vector-path"] !== void 0) {
1999
2048
  const node2 = mergedPath(child, s, m, meta);
2000
2049
  if (node2) out.push(node2);
@@ -2004,6 +2053,8 @@ function parseVectorSvg(svg, options = {}) {
2004
2053
  const children = visit(child, s, m);
2005
2054
  if (!children.length) continue;
2006
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;
2007
2058
  out.push(group);
2008
2059
  continue;
2009
2060
  }
@@ -2029,6 +2080,910 @@ function parseVectorSvg(svg, options = {}) {
2029
2080
  return { width, height, nodes };
2030
2081
  }
2031
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
+
2032
2987
  // src/vector/snap.ts
2033
2988
  var VECTOR_CONSTRUCTION_ANGLES = [0, 45, 90, 135];
2034
2989
  function constrainToLeave(leave, p) {
@@ -2558,14 +3513,14 @@ var VectorSnapTargets = class _VectorSnapTargets {
2558
3513
  * corner) slides along it until one of them is on a target. An axis that snaps to nothing
2559
3514
  * may match another object's width or height instead.
2560
3515
  */
2561
- snapScale(frame, handle, sx, sy, mods, tol) {
2562
- const toDoc = frame.angle ? rotateMatrix(frame.angle) : IDENTITY_MATRIX;
2563
- 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;
2564
3519
  const corner = handle.length === 2;
2565
3520
  const proportional = !!mods.shift;
2566
- const centre = { x: frame.x + frame.width / 2, y: frame.y + frame.height / 2 };
2567
- const origin = mods.alt ? centre : handlePos(opposite(handle), frame);
2568
- 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);
2569
3524
  const d0 = { x: hp.x - origin.x, y: hp.y - origin.y };
2570
3525
  const valid = (n, s) => Number.isFinite(n) && n * s > 0;
2571
3526
  const nearest = (v2, lines) => {
@@ -2591,7 +3546,7 @@ var VectorSnapTargets = class _VectorSnapTargets {
2591
3546
  const q = applyMatrix(toLocal, { x: tx ?? p.x, y: ty ?? p.y });
2592
3547
  let cx = (q.x - origin.x) / d0.x;
2593
3548
  let cy = (q.y - origin.y) / d0.y;
2594
- if (!frame.angle) {
3549
+ if (!frame2.angle) {
2595
3550
  if (tx === null) cx = sx;
2596
3551
  if (ty === null) cy = sy;
2597
3552
  }
@@ -2620,7 +3575,7 @@ var VectorSnapTargets = class _VectorSnapTargets {
2620
3575
  if (!best || (inLine === best.inLine ? moved < best.moved : !inLine)) best = { s, inLine, moved, snap: found2 };
2621
3576
  };
2622
3577
  for (const h of [handle, ...ends]) {
2623
- const hv = handlePos(h, frame);
3578
+ const hv = handlePos(h, frame2);
2624
3579
  const v2 = { x: hv.x - origin.x, y: hv.y - origin.y };
2625
3580
  const base = applyMatrix(toDoc, { x: origin.x + c.x * v2.x, y: origin.y + c.y * v2.y });
2626
3581
  const dir = applyMatrix(toDoc, { x: e.x * v2.x, y: e.y * v2.y });
@@ -2655,25 +3610,25 @@ var VectorSnapTargets = class _VectorSnapTargets {
2655
3610
  const movesY = handle !== "e" && handle !== "w";
2656
3611
  const sized = [];
2657
3612
  if (!proportional) {
2658
- if (movesX && !snap.hx && !(frame.angle && snap.hy) && frame.width > 1e-9) {
2659
- 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);
2660
3615
  if (len2 !== null && len2 > 0) {
2661
- out.sx = Math.sign(out.sx || 1) * (len2 / frame.width);
3616
+ out.sx = Math.sign(out.sx || 1) * (len2 / frame2.width);
2662
3617
  sized.push("x");
2663
3618
  }
2664
3619
  }
2665
- if (movesY && !snap.hy && !(frame.angle && snap.hx) && frame.height > 1e-9) {
2666
- 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);
2667
3622
  if (len2 !== null && len2 > 0) {
2668
- out.sy = Math.sign(out.sy || 1) * (len2 / frame.height);
3623
+ out.sy = Math.sign(out.sy || 1) * (len2 / frame2.height);
2669
3624
  sized.push("y");
2670
3625
  }
2671
3626
  }
2672
3627
  }
2673
- const x0 = origin.x + (frame.x - origin.x) * out.sx;
2674
- const x1 = origin.x + (frame.x + frame.width - origin.x) * out.sx;
2675
- const y0 = origin.y + (frame.y - origin.y) * out.sy;
2676
- 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;
2677
3632
  const box = { x0: Math.min(x0, x1), x1: Math.max(x0, x1), y0: Math.min(y0, y1), y1: Math.max(y0, y1) };
2678
3633
  const page = [
2679
3634
  { x: box.x0, y: box.y0 },
@@ -3962,6 +4917,15 @@ var DEFAULT_VECTOR_PATH_EDIT = {
3962
4917
  fillGap: 0
3963
4918
  };
3964
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
+ }
3965
4929
  var VECTOR_STYLE_KEYS = [
3966
4930
  "fill",
3967
4931
  "stroke",
@@ -4168,11 +5132,11 @@ function getTransformHandlePositions(box) {
4168
5132
  { handle: "rotate", x: x + w / 2, y: y - VECTOR_ROTATE_HANDLE_OFFSET }
4169
5133
  ];
4170
5134
  }
4171
- function getVectorSelectionBoxGeometry(frame, viewport) {
5135
+ function getVectorSelectionBoxGeometry(frame2, viewport) {
4172
5136
  const z = viewport.zoom;
4173
- const box = { x: frame.x * z + viewport.x, y: frame.y * z + viewport.y, width: frame.width * z, height: frame.height * z };
4174
- const rot = rotateMatrix(frame.angle, { x: viewport.x, y: viewport.y });
4175
- 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 };
4176
5140
  const handles = getTransformHandlePositions(box).map((h) => ({ ...h, screen: toScreen2(h) }));
4177
5141
  const corners2 = [
4178
5142
  { x: box.x, y: box.y },
@@ -4181,7 +5145,7 @@ function getVectorSelectionBoxGeometry(frame, viewport) {
4181
5145
  { x: box.x, y: box.y + box.height }
4182
5146
  ].map(toScreen2);
4183
5147
  const r = (n) => Math.round(n * 1e3) / 1e3;
4184
- 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 };
4185
5149
  }
4186
5150
  var RESIZE_CURSOR_ORDER = ["ew-resize", "nwse-resize", "ns-resize", "nesw-resize"];
4187
5151
  var HANDLE_ANGLES = { e: 0, se: 45, s: 90, sw: 135, w: 180, nw: 225, n: 270, ne: 315 };
@@ -4205,7 +5169,8 @@ var TOOL_LABELS = {
4205
5169
  eraser: "Eraser",
4206
5170
  width: "Width",
4207
5171
  shapeBuilder: "Shape Builder",
4208
- paintBucket: "Paint Bucket"
5172
+ paintBucket: "Paint Bucket",
5173
+ blend: "Blend"
4209
5174
  };
4210
5175
  var TOOL_KEYS = {
4211
5176
  v: "select",
@@ -4222,7 +5187,8 @@ var TOOL_KEYS = {
4222
5187
  e: "eraser",
4223
5188
  w: "width",
4224
5189
  m: "shapeBuilder",
4225
- b: "paintBucket"
5190
+ b: "paintBucket",
5191
+ j: "blend"
4226
5192
  };
4227
5193
  function vectorToolLabel(tool) {
4228
5194
  return TOOL_LABELS[tool];
@@ -4242,7 +5208,8 @@ var VECTOR_TOOL_SHORTCUTS = {
4242
5208
  eraser: "E",
4243
5209
  width: "W",
4244
5210
  shapeBuilder: "M",
4245
- paintBucket: "B"
5211
+ paintBucket: "B",
5212
+ blend: "J"
4246
5213
  };
4247
5214
  var VECTOR_TOOL_GLYPHS = {
4248
5215
  select: "\u2196",
@@ -4259,7 +5226,8 @@ var VECTOR_TOOL_GLYPHS = {
4259
5226
  eraser: "\u232B",
4260
5227
  width: "\u21F9",
4261
5228
  shapeBuilder: "\u2295",
4262
- paintBucket: "\u25CD"
5229
+ paintBucket: "\u25CD",
5230
+ blend: "\u29D3"
4263
5231
  };
4264
5232
  var SHAPE_TOOLS = /* @__PURE__ */ new Set(["rect", "ellipse", "polygon", "star", "line"]);
4265
5233
  var BOOLEAN_OPS = {
@@ -4347,6 +5315,9 @@ var VectorEditorController = class {
4347
5315
  /** Paint bucket map of the visible outlines. */
4348
5316
  fillCache = null;
4349
5317
  builderPreview = null;
5318
+ blendStart = null;
5319
+ /** Options new blends get (the toolbars edit them along with the selected blend's). */
5320
+ blendDefaults = {};
4350
5321
  /** What the highlight shows, to skip notifying when the pointer stays over the same face. */
4351
5322
  builderHoverKey = null;
4352
5323
  createId;
@@ -4361,6 +5332,7 @@ var VectorEditorController = class {
4361
5332
  constructor(options = {}) {
4362
5333
  this.history = options.history ?? new HistoryManager();
4363
5334
  this.doc = options.document ? cloneVectorDocument(options.document) : { width: 800, height: 600, nodes: [] };
5335
+ this.doc = this.refreshed(this.doc);
4364
5336
  this.tool = options.tool ?? "select";
4365
5337
  this.defaults = { ...DEFAULT_DEFAULTS, ...options.defaults ?? {} };
4366
5338
  this.viewport = { x: 0, y: 0, zoom: 1, ...options.viewport ?? {} };
@@ -4425,6 +5397,8 @@ var VectorEditorController = class {
4425
5397
  pen: this.pen,
4426
5398
  pointer: this.pointer,
4427
5399
  builder: this.builderPreview,
5400
+ blendStart: this.blendStart,
5401
+ blendDefaults: this.blendDefaults,
4428
5402
  canUndo: h.canUndo,
4429
5403
  canRedo: h.canRedo,
4430
5404
  undoLabel: h.undoName,
@@ -4432,7 +5406,15 @@ var VectorEditorController = class {
4432
5406
  revision: this.revision
4433
5407
  };
4434
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
+ }
4435
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;
4436
5418
  this.revision++;
4437
5419
  this.state = this.computeState();
4438
5420
  if (docChanged) this.onChange?.(this.doc);
@@ -4488,6 +5470,8 @@ var VectorEditorController = class {
4488
5470
  return this.builderPreview.mode === "erase" ? "not-allowed" : "cell";
4489
5471
  case "paintBucket":
4490
5472
  return this.builderPreview ? "cell" : "crosshair";
5473
+ case "blend":
5474
+ return h?.nodeId ? this.blendStart && this.blendStart.nodeId !== h.nodeId ? "copy" : "pointer" : "crosshair";
4491
5475
  default:
4492
5476
  return "crosshair";
4493
5477
  }
@@ -4523,6 +5507,7 @@ var VectorEditorController = class {
4523
5507
  }
4524
5508
  /** Records the change from `before` to the current state as one undo step. */
4525
5509
  commit(name, before) {
5510
+ this.doc = this.refreshed(this.doc);
4526
5511
  const after = this.snapshot();
4527
5512
  if (before.doc === after.doc) {
4528
5513
  this.notify();
@@ -4625,6 +5610,7 @@ var VectorEditorController = class {
4625
5610
  this.widthEdit = null;
4626
5611
  this.builderPreview = null;
4627
5612
  this.builderHoverKey = null;
5613
+ this.blendStart = null;
4628
5614
  if (tool !== "direct") {
4629
5615
  this.anchors = [];
4630
5616
  this.editingId = null;
@@ -5249,24 +6235,24 @@ var VectorEditorController = class {
5249
6235
  return this.transform(translateMatrix(dx, dy), { ids: options.ids, label: "Move" });
5250
6236
  }
5251
6237
  /** The selection frame's centre in document space. */
5252
- frameCenter(frame) {
5253
- 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 });
5254
6240
  }
5255
6241
  /**
5256
6242
  * Scales along the selection box's own axes (rotated with the selection when its nodes
5257
6243
  * share a rotation) around `origin` (default: the box centre, in document space).
5258
6244
  */
5259
6245
  scale(sx, sy = sx, options = {}) {
5260
- const frame = this.frameOf(options.ids ?? this.selectedRoots());
5261
- if (!frame) return { ok: false, message: "Nothing is selected." };
5262
- const origin = applyMatrix(rotateMatrix(-frame.angle), options.origin ?? this.frameCenter(frame));
5263
- 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" });
5264
6250
  }
5265
6251
  /** Rotates clockwise by `degrees` around `origin` (default: the selection centre). */
5266
6252
  rotate(degrees, options = {}) {
5267
- const frame = this.frameOf(options.ids ?? this.selectedRoots());
5268
- if (!frame) return { ok: false, message: "Nothing is selected." };
5269
- 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);
5270
6256
  return this.transform(rotateMatrix(degrees, origin), { ids: options.ids, label: "Rotate" });
5271
6257
  }
5272
6258
  /** Sets each selected (or given) node's rotation to `degrees`, turning it around its own centre. */
@@ -5275,21 +6261,21 @@ var VectorEditorController = class {
5275
6261
  if (!ids.length) return { ok: false, message: "Nothing is selected." };
5276
6262
  this.change("Set Rotation", () => {
5277
6263
  for (const id of ids) {
5278
- const frame = this.frameOf([id]);
5279
- if (!frame) continue;
5280
- const delta = normalizeAngle(degrees - frame.angle);
5281
- 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)));
5282
6268
  }
5283
6269
  });
5284
6270
  return { ok: true };
5285
6271
  }
5286
6272
  /** Mirrors along the selection box's own axes. */
5287
6273
  flip(axis, options = {}) {
5288
- const frame = this.frameOf(options.ids ?? this.selectedRoots());
5289
- if (!frame) return { ok: false, message: "Nothing is selected." };
5290
- 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 };
5291
6277
  const m = axis === "horizontal" ? scaleMatrix(-1, 1, c) : scaleMatrix(1, -1, c);
5292
- 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" });
5293
6279
  }
5294
6280
  /**
5295
6281
  * Aligns the selected nodes to their combined bounds, or to the artboard when one node is
@@ -5416,7 +6402,7 @@ var VectorEditorController = class {
5416
6402
  const opacity = n.opacity;
5417
6403
  const m = nodeMatrix(n);
5418
6404
  out.push(
5419
- ...n.children.map((c) => n.transform ? transformVectorNode(c, m) : c).map((c) => opacity !== void 0 && opacity < 1 ? { ...c, opacity: (c.opacity ?? 1) * opacity } : c)
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)
5420
6406
  );
5421
6407
  } else if (n.type === "group") {
5422
6408
  const children = flatten2(n.children);
@@ -5580,6 +6566,173 @@ var VectorEditorController = class {
5580
6566
  return nodes.map((n) => n.id);
5581
6567
  }
5582
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
+ }
5583
6736
  shapeBuilderMap(ids) {
5584
6737
  const roots = ids ?? this.selectedRoots();
5585
6738
  const key = roots.join("\0");
@@ -6318,9 +7471,9 @@ var VectorEditorController = class {
6318
7471
  }
6319
7472
  hitTransformHandle(screen) {
6320
7473
  if (this.tool !== "select" || !this.selection.length) return null;
6321
- const frame = this.frameOf(this.selectedRoots());
6322
- if (!frame) return null;
6323
- 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);
6324
7477
  const r = this.hitRadius + 2;
6325
7478
  const usable2 = box.width < 2 * r || box.height < 2 * r ? handles.filter((h) => h.handle.length === 2 || h.handle === "rotate") : handles;
6326
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;
@@ -6432,6 +7585,11 @@ var VectorEditorController = class {
6432
7585
  const seg = this.nearestOutline(p);
6433
7586
  return seg ? { nodeId: seg.nodeId, segment: seg } : null;
6434
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
+ }
6435
7593
  case "width": {
6436
7594
  const hit = this.hitWidthHandle(screen);
6437
7595
  if (hit) return { nodeId: hit.ref.nodeId, widthPoint: hit.ref };
@@ -6658,6 +7816,8 @@ var VectorEditorController = class {
6658
7816
  case "shapeBuilder":
6659
7817
  case "paintBucket":
6660
7818
  return this.downBuilder(p, input);
7819
+ case "blend":
7820
+ return this.downBlend(screen, p);
6661
7821
  case "scissors": {
6662
7822
  const seg = this.nearestOutline(p);
6663
7823
  if (seg) this.cutPathAt(seg.nodeId, seg.point);
@@ -6673,13 +7833,53 @@ var VectorEditorController = class {
6673
7833
  return this.downShape(p, input);
6674
7834
  }
6675
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
+ }
6676
7876
  downSelect(screen, p, input) {
6677
7877
  const handle = this.hitTransformHandle(screen);
6678
7878
  if (handle) {
6679
- const frame = this.frameOf(this.selectedRoots());
7879
+ const frame2 = this.frameOf(this.selectedRoots());
6680
7880
  const before = this.snapshot();
6681
- const center = applyMatrix(rotateMatrix(frame.angle), { x: frame.x + frame.width / 2, y: frame.y + frame.height / 2 });
6682
- 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 };
6683
7883
  this.interaction = handle === "rotate" ? { type: "rotate", angle: 0 } : { type: "scale", handle, sx: 1, sy: 1 };
6684
7884
  this.notify();
6685
7885
  return;
@@ -7474,7 +8674,10 @@ var VectorEditorController = class {
7474
8674
  this.widthPoints = [];
7475
8675
  this.notify();
7476
8676
  } else if (this.pen) this.finishPath();
7477
- 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) {
7478
8681
  this.anchors = [];
7479
8682
  this.notify();
7480
8683
  } else if (this.tool !== "select") this.setTool("select");
@@ -7800,8 +9003,79 @@ function getVectorOverlayRenderData(state) {
7800
9003
  status = state.builder.mode === "erase" ? `Delete ${n} ${n === 1 ? "face" : "faces"}` : `Merge ${n} ${n === 1 ? "face" : "faces"}`;
7801
9004
  }
7802
9005
  }
7803
- 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()];
7804
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" };
7805
9079
  var VECTOR_OVERLAY_THEME = {
7806
9080
  accent: "#0ea5e9",
7807
9081
  /** Smart guide lines and labels. */
@@ -7986,6 +9260,18 @@ var WIDTH_POINTS = {
7986
9260
  },
7987
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."
7988
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
+ };
7989
9275
  var PAINT = {
7990
9276
  description: 'A CSS colour, `null` for none, or a gradient `{ type: "linear", angle, stops: [{ offset, color }] }`.'
7991
9277
  };
@@ -8050,7 +9336,9 @@ function createVectorCommands(controller, options = {}) {
8050
9336
  visible: n.visible !== false,
8051
9337
  locked: !!n.locked,
8052
9338
  selected: state.selection.includes(n.id),
8053
- ...n.type === "path" ? { fill: n.fill ?? null, stroke: n.stroke ?? null, strokeWidth: n.strokeWidth ?? 1, d: vectorPathData(n, 2) } : { children: n.children.length }
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 } : {}
8054
9342
  });
8055
9343
  if (n.type === "group") visit(n.children, depth + 1);
8056
9344
  }
@@ -8295,6 +9583,86 @@ function createVectorCommands(controller, options = {}) {
8295
9583
  return { id };
8296
9584
  }
8297
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
+ },
8298
9666
  {
8299
9667
  ...base,
8300
9668
  id: `${prefix}pathfinder`,
@@ -8642,6 +10010,8 @@ function createVectorCommands(controller, options = {}) {
8642
10010
  ];
8643
10011
  }
8644
10012
  export {
10013
+ DEFAULT_VECTOR_BLEND_DISTANCE,
10014
+ DEFAULT_VECTOR_BLEND_STEPS,
8645
10015
  DEFAULT_VECTOR_PATH_EDIT,
8646
10016
  DEFAULT_VECTOR_SNAP,
8647
10017
  IDENTITY_MATRIX,
@@ -8649,6 +10019,10 @@ export {
8649
10019
  LazyMouse,
8650
10020
  VECTOR_ARROWHEADS,
8651
10021
  VECTOR_ARROWHEAD_LABELS,
10022
+ VECTOR_BLEND_MAX_SMOOTH_STEPS,
10023
+ VECTOR_BLEND_MAX_STEPS,
10024
+ VECTOR_BLEND_SPACINGS,
10025
+ VECTOR_BLEND_SPACING_LABELS,
8652
10026
  VECTOR_BUILDER_HATCH_GAP,
8653
10027
  VECTOR_CLIPBOARD_TYPE,
8654
10028
  VECTOR_CONSTRUCTION_ANGLES,
@@ -8682,6 +10056,7 @@ export {
8682
10056
  addBuilderHit,
8683
10057
  applyMatrix,
8684
10058
  applyMatrixLinear,
10059
+ applyVectorBlendOptions,
8685
10060
  applyVectorCornersAttribute,
8686
10061
  arrowheadInset,
8687
10062
  arrowheadPath,
@@ -8700,7 +10075,9 @@ export {
8700
10075
  cornerBisectorPoint,
8701
10076
  cornerRadiusChange,
8702
10077
  cornerSetback,
10078
+ correspondMorphContours,
8703
10079
  createVectorCommands,
10080
+ createVectorMorph,
8704
10081
  curvatureMoveAnchor,
8705
10082
  cutSubpathAt,
8706
10083
  dashIntervals,
@@ -8710,15 +10087,19 @@ export {
8710
10087
  eraseFromFill,
8711
10088
  eraseFromOutline,
8712
10089
  eraserRegion,
10090
+ evaluateVectorPathKeyframes,
10091
+ expandVectorBlend,
8713
10092
  expandVectorCorners,
8714
10093
  expandVectorCornersOf,
8715
10094
  findVectorNode,
8716
10095
  fitCornerRadius,
10096
+ generateVectorBlendChildren,
8717
10097
  generateWidthProfile,
8718
10098
  getAnchor,
8719
10099
  getAnchorWorld,
8720
10100
  getEditablePathIds,
8721
10101
  getTransformHandlePositions,
10102
+ getVectorBlendControls,
8722
10103
  getVectorCornerWidgets,
8723
10104
  getVectorOverlayRenderData,
8724
10105
  getVectorSceneRenderData,
@@ -8737,10 +10118,18 @@ export {
8737
10118
  inferAnchorType,
8738
10119
  insertAnchor,
8739
10120
  insertWidthPoint,
10121
+ interpolateMorphContours,
10122
+ interpolateRigidMatrix,
10123
+ interpolateVectorColor,
10124
+ interpolateVectorPaint,
10125
+ interpolateVectorPaths,
10126
+ interpolateVectorStyle,
10127
+ interpolateWidthProfiles,
8740
10128
  invertMatrix,
8741
10129
  isClosedBezierShape,
8742
10130
  isIdentityMatrix,
8743
10131
  isUniformWidthProfile,
10132
+ isVectorBlendGroup,
8744
10133
  isVectorTextTarget,
8745
10134
  joinSubpaths,
8746
10135
  knifePath,
@@ -8748,6 +10137,8 @@ export {
8748
10137
  locateVectorNode,
8749
10138
  matrixRotation,
8750
10139
  mirrorWidthProfile,
10140
+ morphContourToSubpath,
10141
+ morphPathsCross,
8751
10142
  moveAnchorHandle,
8752
10143
  multiplyMatrices,
8753
10144
  nearestSegment,
@@ -8762,11 +10153,17 @@ export {
8762
10153
  pressureWidthProfile,
8763
10154
  recognizeInkShape,
8764
10155
  rectSubpath,
10156
+ refineMorphContour,
10157
+ refreshVectorBlends,
10158
+ releaseVectorBlend,
8765
10159
  removeAnchorKeepingShape,
8766
10160
  removeAnchors,
8767
10161
  removeAnchorsKeepingShape,
8768
10162
  removeRedundantPoints,
8769
10163
  removeWidthPoints,
10164
+ resampleGradientStops,
10165
+ resolveVectorBlendOptions,
10166
+ reverseMorphContour,
8770
10167
  reverseSubpath,
8771
10168
  reverseWidthProfile,
8772
10169
  rotateMatrix,
@@ -8789,6 +10186,7 @@ export {
8789
10186
  subpathPointAt,
8790
10187
  subpathSegment,
8791
10188
  subpathSegmentCount,
10189
+ subpathToMorphContour,
8792
10190
  subpathsToBezier,
8793
10191
  taperWaveFactor,
8794
10192
  toVectorKeyInput,
@@ -8797,9 +10195,17 @@ export {
8797
10195
  transformVectorNode,
8798
10196
  translateAnchor,
8799
10197
  translateMatrix,
10198
+ turnMorphContour,
8800
10199
  unionBounds,
8801
10200
  variableStrokeOutline,
8802
10201
  variableStrokeRings,
10202
+ vectorBlendColorDistance,
10203
+ vectorBlendKeyTargets,
10204
+ vectorBlendKeys,
10205
+ vectorBlendRail,
10206
+ vectorBlendRailAt,
10207
+ vectorBlendSpine,
10208
+ vectorBlendStepCount,
8803
10209
  vectorBuilderErasedEdges,
8804
10210
  vectorCornerAt,
8805
10211
  vectorCorners,