@sliiu/present-transition-fx 0.0.0-stage → 0.1.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.
Files changed (48) hide show
  1. package/COPYRIGHT +5 -0
  2. package/LICENSE +661 -0
  3. package/THIRD-PARTY-NOTICES.md +221 -0
  4. package/dist/cjs/2d.js +487 -0
  5. package/dist/cjs/3d.js +566 -0
  6. package/dist/cjs/geometry.js +141 -0
  7. package/dist/cjs/index.js +98 -0
  8. package/dist/cjs/morph-audit.js +603 -0
  9. package/dist/cjs/morph-match.js +103 -0
  10. package/dist/cjs/morph-path.js +649 -0
  11. package/dist/cjs/morph.js +223 -0
  12. package/dist/cjs/package.json +1 -0
  13. package/dist/cjs/registry.js +130 -0
  14. package/dist/cjs/snapshot.js +420 -0
  15. package/dist/cjs/twin.js +88 -0
  16. package/dist/cjs/types.js +2 -0
  17. package/dist/cjs/util.js +50 -0
  18. package/dist/cjs/webgl.js +271 -0
  19. package/dist/esm/2d.js +454 -0
  20. package/dist/esm/3d.js +538 -0
  21. package/dist/esm/geometry.js +127 -0
  22. package/dist/esm/index.js +6 -0
  23. package/dist/esm/morph-audit.js +558 -0
  24. package/dist/esm/morph-match.js +97 -0
  25. package/dist/esm/morph-path.js +636 -0
  26. package/dist/esm/morph.js +219 -0
  27. package/dist/esm/registry.js +91 -0
  28. package/dist/esm/snapshot.js +414 -0
  29. package/dist/esm/twin.js +84 -0
  30. package/dist/esm/types.js +1 -0
  31. package/dist/esm/util.js +43 -0
  32. package/dist/esm/webgl.js +231 -0
  33. package/dist/types/2d.d.ts +40 -0
  34. package/dist/types/3d.d.ts +38 -0
  35. package/dist/types/geometry.d.ts +38 -0
  36. package/dist/types/index.d.ts +7 -0
  37. package/dist/types/morph-audit.d.ts +127 -0
  38. package/dist/types/morph-match.d.ts +28 -0
  39. package/dist/types/morph-path.d.ts +37 -0
  40. package/dist/types/morph.d.ts +2 -0
  41. package/dist/types/registry.d.ts +6 -0
  42. package/dist/types/snapshot.d.ts +19 -0
  43. package/dist/types/twin.d.ts +2 -0
  44. package/dist/types/types.d.ts +13 -0
  45. package/dist/types/util.d.ts +12 -0
  46. package/dist/types/webgl.d.ts +5 -0
  47. package/package.json +26 -4
  48. package/README.md +0 -3
@@ -0,0 +1,636 @@
1
+ /** SVG path helpers for Morph transitions (normalize + interpolate). */
2
+ function tokenizePath(d) {
3
+ const out = [];
4
+ const re = /[a-zA-Z]|-?\d*\.?\d+(?:e[-+]?\d+)?/gi;
5
+ let m;
6
+ while ((m = re.exec(d)) !== null) {
7
+ const t = m[0];
8
+ if (/^[a-zA-Z]$/.test(t))
9
+ out.push(t);
10
+ else
11
+ out.push(Number(t));
12
+ }
13
+ return out;
14
+ }
15
+ export function parsePathCommands(d) {
16
+ const tokens = tokenizePath(d);
17
+ const cmds = [];
18
+ let i = 0;
19
+ while (i < tokens.length) {
20
+ const t = tokens[i];
21
+ if (typeof t !== "string") {
22
+ i++;
23
+ continue;
24
+ }
25
+ const cmd = t;
26
+ i++;
27
+ const n = paramCount(cmd);
28
+ const args = [];
29
+ if (n === 0) {
30
+ cmds.push({ cmd, args });
31
+ continue;
32
+ }
33
+ while (i < tokens.length && typeof tokens[i] === "number") {
34
+ args.push(tokens[i]);
35
+ i++;
36
+ if (args.length >= n) {
37
+ cmds.push({ cmd, args: args.splice(0, n) });
38
+ if (args.length)
39
+ continue;
40
+ break;
41
+ }
42
+ }
43
+ if (args.length)
44
+ cmds.push({ cmd, args });
45
+ }
46
+ return cmds;
47
+ }
48
+ function paramCount(cmd) {
49
+ switch (cmd.toUpperCase()) {
50
+ case "M":
51
+ case "L":
52
+ case "T":
53
+ return 2;
54
+ case "H":
55
+ case "V":
56
+ return 1;
57
+ case "C":
58
+ return 6;
59
+ case "S":
60
+ case "Q":
61
+ return 4;
62
+ case "A":
63
+ return 7;
64
+ case "Z":
65
+ return 0;
66
+ default:
67
+ return 0;
68
+ }
69
+ }
70
+ /** Convert relative path commands to absolute (uppercase) equivalents. */
71
+ export function pathToAbsolute(commands) {
72
+ let cx = 0;
73
+ let cy = 0;
74
+ let sx = 0;
75
+ let sy = 0;
76
+ let prevC2x = cx;
77
+ let prevC2y = cy;
78
+ const out = [];
79
+ for (const { cmd, args } of commands) {
80
+ const rel = cmd === cmd.toLowerCase();
81
+ const C = cmd.toUpperCase();
82
+ if (C === "Z") {
83
+ cx = sx;
84
+ cy = sy;
85
+ out.push({ cmd: "Z", args: [] });
86
+ continue;
87
+ }
88
+ if (C === "M") {
89
+ for (let i = 0; i < args.length; i += 2) {
90
+ const x = rel ? cx + args[i] : args[i];
91
+ const y = rel ? cy + args[i + 1] : args[i + 1];
92
+ cx = x;
93
+ cy = y;
94
+ if (i === 0) {
95
+ sx = x;
96
+ sy = y;
97
+ out.push({ cmd: "M", args: [x, y] });
98
+ }
99
+ else {
100
+ out.push({ cmd: "L", args: [x, y] });
101
+ }
102
+ }
103
+ continue;
104
+ }
105
+ if (C === "L") {
106
+ for (let i = 0; i < args.length; i += 2) {
107
+ const x = rel ? cx + args[i] : args[i];
108
+ const y = rel ? cy + args[i + 1] : args[i + 1];
109
+ cx = x;
110
+ cy = y;
111
+ out.push({ cmd: "L", args: [x, y] });
112
+ }
113
+ continue;
114
+ }
115
+ if (C === "H") {
116
+ for (const a of args) {
117
+ cx = rel ? cx + a : a;
118
+ out.push({ cmd: "L", args: [cx, cy] });
119
+ }
120
+ continue;
121
+ }
122
+ if (C === "V") {
123
+ for (const a of args) {
124
+ cy = rel ? cy + a : a;
125
+ out.push({ cmd: "L", args: [cx, cy] });
126
+ }
127
+ continue;
128
+ }
129
+ if (C === "C") {
130
+ for (let i = 0; i < args.length; i += 6) {
131
+ const x1 = rel ? cx + args[i] : args[i];
132
+ const y1 = rel ? cy + args[i + 1] : args[i + 1];
133
+ const x2 = rel ? cx + args[i + 2] : args[i + 2];
134
+ const y2 = rel ? cy + args[i + 3] : args[i + 3];
135
+ const x = rel ? cx + args[i + 4] : args[i + 4];
136
+ const y = rel ? cy + args[i + 5] : args[i + 5];
137
+ out.push({ cmd: "C", args: [x1, y1, x2, y2, x, y] });
138
+ prevC2x = x2;
139
+ prevC2y = y2;
140
+ cx = x;
141
+ cy = y;
142
+ }
143
+ continue;
144
+ }
145
+ if (C === "Q") {
146
+ for (let i = 0; i < args.length; i += 4) {
147
+ const x1 = rel ? cx + args[i] : args[i];
148
+ const y1 = rel ? cy + args[i + 1] : args[i + 1];
149
+ const x = rel ? cx + args[i + 2] : args[i + 2];
150
+ const y = rel ? cy + args[i + 3] : args[i + 3];
151
+ out.push({ cmd: "Q", args: [x1, y1, x, y] });
152
+ prevC2x = x1;
153
+ prevC2y = y1;
154
+ cx = x;
155
+ cy = y;
156
+ }
157
+ continue;
158
+ }
159
+ if (C === "S") {
160
+ for (let i = 0; i < args.length; i += 4) {
161
+ const x2 = rel ? cx + args[i] : args[i];
162
+ const y2 = rel ? cy + args[i + 1] : args[i + 1];
163
+ const x = rel ? cx + args[i + 2] : args[i + 2];
164
+ const y = rel ? cy + args[i + 3] : args[i + 3];
165
+ const x1 = 2 * cx - prevC2x;
166
+ const y1 = 2 * cy - prevC2y;
167
+ out.push({ cmd: "C", args: [x1, y1, x2, y2, x, y] });
168
+ prevC2x = x2;
169
+ prevC2y = y2;
170
+ cx = x;
171
+ cy = y;
172
+ }
173
+ continue;
174
+ }
175
+ if (C === "T") {
176
+ for (let i = 0; i < args.length; i += 2) {
177
+ const x = rel ? cx + args[i] : args[i];
178
+ const y = rel ? cy + args[i + 1] : args[i + 1];
179
+ const x1 = 2 * cx - prevC2x;
180
+ const y1 = 2 * cy - prevC2y;
181
+ out.push({ cmd: "Q", args: [x1, y1, x, y] });
182
+ prevC2x = x1;
183
+ prevC2y = y1;
184
+ cx = x;
185
+ cy = y;
186
+ }
187
+ continue;
188
+ }
189
+ if (C === "A") {
190
+ for (let i = 0; i < args.length; i += 7) {
191
+ const rx = args[i];
192
+ const ry = args[i + 1];
193
+ const rot = args[i + 2];
194
+ const laf = args[i + 3];
195
+ const sf = args[i + 4];
196
+ const x = rel ? cx + args[i + 5] : args[i + 5];
197
+ const y = rel ? cy + args[i + 6] : args[i + 6];
198
+ out.push({ cmd: "A", args: [rx, ry, rot, laf, sf, x, y] });
199
+ cx = x;
200
+ cy = y;
201
+ }
202
+ continue;
203
+ }
204
+ }
205
+ return out;
206
+ }
207
+ function commandsToNumbers(commands) {
208
+ const cmds = [];
209
+ const nums = [];
210
+ for (const c of commands) {
211
+ cmds.push(c.cmd);
212
+ nums.push(...c.args);
213
+ }
214
+ return { cmds, nums };
215
+ }
216
+ function lerp(a, b, t) {
217
+ return a + (b - a) * t;
218
+ }
219
+ function buildPathFromInterp(cmds, from, to, t) {
220
+ const out = [];
221
+ let p = 0;
222
+ for (const c of cmds) {
223
+ out.push(c);
224
+ const n = paramCount(c);
225
+ for (let i = 0; i < n; i++) {
226
+ // Arc large-arc/sweep flags must be a bare 0 or 1; "1.000" stops the browser's path parser.
227
+ const isArcFlag = c.toUpperCase() === "A" && (i % 7 === 3 || i % 7 === 4);
228
+ out.push(isArcFlag ? String(Math.round(t < 0.5 ? from[p] : to[p])) : lerp(from[p], to[p], t).toFixed(3));
229
+ p++;
230
+ }
231
+ }
232
+ return out.join(" ");
233
+ }
234
+ function asPolyline(commands) {
235
+ const points = [];
236
+ let closed = false;
237
+ for (const { cmd, args } of commands) {
238
+ if (cmd === "M" || cmd === "L") {
239
+ points.push({ x: args[0], y: args[1] });
240
+ }
241
+ else if (cmd === "Z") {
242
+ closed = true;
243
+ }
244
+ else {
245
+ return null;
246
+ }
247
+ }
248
+ if (points.length < 2)
249
+ return null;
250
+ return { points, closed };
251
+ }
252
+ function perimeter(points, closed) {
253
+ let sum = 0;
254
+ const n = points.length;
255
+ const last = closed ? n : n - 1;
256
+ for (let i = 0; i < last; i++) {
257
+ const a = points[i];
258
+ const b = points[(i + 1) % n];
259
+ sum += Math.hypot(b.x - a.x, b.y - a.y);
260
+ }
261
+ return sum;
262
+ }
263
+ function pointAt(points, closed, dist) {
264
+ let acc = 0;
265
+ const n = points.length;
266
+ const segCount = closed ? n : n - 1;
267
+ for (let i = 0; i < segCount; i++) {
268
+ const a = points[i];
269
+ const b = points[(i + 1) % n];
270
+ const len = Math.hypot(b.x - a.x, b.y - a.y);
271
+ if (acc + len >= dist || i === segCount - 1) {
272
+ const t = len > 0 ? Math.min(1, (dist - acc) / len) : 0;
273
+ return { x: lerp(a.x, b.x, t), y: lerp(a.y, b.y, t) };
274
+ }
275
+ acc += len;
276
+ }
277
+ return points[points.length - 1];
278
+ }
279
+ function resamplePolyline(points, closed, count) {
280
+ const per = perimeter(points, closed);
281
+ if (per <= 0)
282
+ return points.slice(0, count);
283
+ const out = [];
284
+ for (let i = 0; i < count; i++) {
285
+ const d = closed ? (per * i) / count : count <= 1 ? 0 : (per * i) / (count - 1);
286
+ out.push(pointAt(points, closed, d));
287
+ }
288
+ return out;
289
+ }
290
+ function polylinePath(points, closed) {
291
+ if (!points.length)
292
+ return "";
293
+ const parts = [`M ${points[0].x.toFixed(3)} ${points[0].y.toFixed(3)}`];
294
+ for (let i = 1; i < points.length; i++) {
295
+ parts.push(`L ${points[i].x.toFixed(3)} ${points[i].y.toFixed(3)}`);
296
+ }
297
+ if (closed)
298
+ parts.push("Z");
299
+ return parts.join(" ");
300
+ }
301
+ /** Open line path in local coordinates. */
302
+ export function linePathFromPoints(x1, y1, x2, y2) {
303
+ return `M ${x1.toFixed(3)} ${y1.toFixed(3)} L ${x2.toFixed(3)} ${y2.toFixed(3)}`;
304
+ }
305
+ /** Closed rectangle path in local coordinates (0,0)–(w,h). */
306
+ export function rectPathFromSize(w, h) {
307
+ if (w <= 0 || h <= 0)
308
+ return "";
309
+ return polylinePath([
310
+ { x: 0, y: 0 },
311
+ { x: w, y: 0 },
312
+ { x: w, y: h },
313
+ { x: 0, y: h },
314
+ ], true);
315
+ }
316
+ /** Closed ellipse path approximated as a polyline in local bbox (w×h). */
317
+ export function ellipsePathFromSize(w, h, segments = 16) {
318
+ if (w <= 0 || h <= 0)
319
+ return "";
320
+ const cx = w / 2;
321
+ const cy = h / 2;
322
+ const rx = w / 2;
323
+ const ry = h / 2;
324
+ const pts = [];
325
+ for (let i = 0; i < segments; i++) {
326
+ const ang = (2 * Math.PI * i) / segments;
327
+ pts.push({ x: cx + rx * Math.cos(ang), y: cy + ry * Math.sin(ang) });
328
+ }
329
+ return polylinePath(pts, true);
330
+ }
331
+ /** Convert SVG polygon/polyline `points` attribute to a path `d` string. */
332
+ export function polygonPointsToPath(points, closed) {
333
+ const pts = points
334
+ .trim()
335
+ .split(/\s+|,/)
336
+ .map(Number)
337
+ .filter((n) => Number.isFinite(n));
338
+ if (pts.length < 2)
339
+ return "";
340
+ const out = [`M ${pts[0]} ${pts[1]}`];
341
+ for (let i = 2; i < pts.length; i += 2)
342
+ out.push(`L ${pts[i]} ${pts[i + 1]}`);
343
+ if (closed)
344
+ out.push("Z");
345
+ return out.join(" ");
346
+ }
347
+ function reversePoints(points) {
348
+ return [...points].reverse();
349
+ }
350
+ function rotatePoints(points, offset) {
351
+ if (!points.length || offset === 0)
352
+ return points;
353
+ const o = ((offset % points.length) + points.length) % points.length;
354
+ if (o === 0)
355
+ return points;
356
+ return [...points.slice(o), ...points.slice(0, o)];
357
+ }
358
+ function polylinePairScore(a, b) {
359
+ const n = Math.min(a.length, b.length);
360
+ let score = 0;
361
+ for (let i = 0; i < n; i++)
362
+ score += Math.hypot(a[i].x - b[i].x, a[i].y - b[i].y);
363
+ return score;
364
+ }
365
+ /**
366
+ * Rotate / reverse `b` so closed polylines align for smoother morph tweens.
367
+ * Exported for regression tests.
368
+ */
369
+ export function alignClosedPolylines(a, b) {
370
+ if (!a.length || a.length !== b.length || a.length < 3)
371
+ return { a, b };
372
+ let bestB = b;
373
+ let bestScore = Infinity;
374
+ for (const candidate of [b, reversePoints(b)]) {
375
+ for (let offset = 0; offset < candidate.length; offset++) {
376
+ const rotated = rotatePoints(candidate, offset);
377
+ const score = polylinePairScore(a, rotated);
378
+ if (score < bestScore) {
379
+ bestScore = score;
380
+ bestB = rotated;
381
+ }
382
+ }
383
+ }
384
+ return { a, b: bestB };
385
+ }
386
+ /** Align open polylines by optional reverse (same point count). */
387
+ function alignOpenPolylines(a, b) {
388
+ if (!a.length || a.length !== b.length)
389
+ return b;
390
+ const rev = reversePoints(b);
391
+ return polylinePairScore(a, rev) < polylinePairScore(a, b) ? rev : b;
392
+ }
393
+ function alignResampledPolylines(a, b, closed) {
394
+ if (!a.length || a.length !== b.length)
395
+ return b;
396
+ if (closed)
397
+ return alignClosedPolylines(a, b).b;
398
+ return alignOpenPolylines(a, b);
399
+ }
400
+ function cubicAt(p0, p1, p2, p3, t) {
401
+ const u = 1 - t;
402
+ const uu = u * u;
403
+ const tt = t * t;
404
+ return {
405
+ x: uu * u * p0.x + 3 * uu * t * p1.x + 3 * u * tt * p2.x + tt * t * p3.x,
406
+ y: uu * u * p0.y + 3 * uu * t * p1.y + 3 * u * tt * p2.y + tt * t * p3.y,
407
+ };
408
+ }
409
+ function quadAt(p0, p1, p2, t) {
410
+ const u = 1 - t;
411
+ return {
412
+ x: u * u * p0.x + 2 * u * t * p1.x + t * t * p2.x,
413
+ y: u * u * p0.y + 2 * u * t * p1.y + t * t * p2.y,
414
+ };
415
+ }
416
+ /** SVG arc endpoint → center parameterization (W3C implementation notes). */
417
+ function arcCenter(x0, y0, rx, ry, phi, largeArc, sweep, x1, y1) {
418
+ if (!Number.isFinite(rx) || !Number.isFinite(ry) || Math.abs(rx) < 1e-6 || Math.abs(ry) < 1e-6)
419
+ return null;
420
+ if (Math.hypot(x0 - x1, y0 - y1) < 1e-4)
421
+ return null;
422
+ const cosPhi = Math.cos(phi);
423
+ const sinPhi = Math.sin(phi);
424
+ const dx = (x0 - x1) / 2;
425
+ const dy = (y0 - y1) / 2;
426
+ const x1p = cosPhi * dx + sinPhi * dy;
427
+ const y1p = -sinPhi * dx + cosPhi * dy;
428
+ let rx2 = Math.abs(rx);
429
+ let ry2 = Math.abs(ry);
430
+ const lam = (x1p * x1p) / (rx2 * rx2) + (y1p * y1p) / (ry2 * ry2);
431
+ if (lam > 1) {
432
+ const s = Math.sqrt(lam);
433
+ rx2 *= s;
434
+ ry2 *= s;
435
+ }
436
+ const sq = Math.max(0, (rx2 * rx2 * ry2 * ry2 - rx2 * rx2 * y1p * y1p - ry2 * ry2 * x1p * x1p) /
437
+ (rx2 * rx2 * y1p * y1p + ry2 * ry2 * x1p * x1p));
438
+ const coef = (largeArc === sweep ? -1 : 1) * Math.sqrt(sq);
439
+ const cxp = (coef * rx2 * y1p) / ry2;
440
+ const cyp = (coef * -ry2 * x1p) / rx2;
441
+ const cx = cosPhi * cxp - sinPhi * cyp + (x0 + x1) / 2;
442
+ const cy = sinPhi * cxp + cosPhi * cyp + (y0 + y1) / 2;
443
+ const angleBetween = (ux, uy, vx, vy) => Math.atan2(ux * vy - uy * vx, ux * vx + uy * vy);
444
+ const theta1 = angleBetween(1, 0, (x1p - cxp) / rx2, (y1p - cyp) / ry2);
445
+ let delta = angleBetween((x1p - cxp) / rx2, (y1p - cyp) / ry2, (-x1p - cxp) / rx2, (-y1p - cyp) / ry2);
446
+ if (sweep === 0 && delta > 0)
447
+ delta -= 2 * Math.PI;
448
+ if (sweep === 1 && delta < 0)
449
+ delta += 2 * Math.PI;
450
+ return { cx, cy, start: theta1, delta };
451
+ }
452
+ function lineSamples(x0, y0, x1, y1, segments) {
453
+ const n = Math.max(2, segments);
454
+ const out = [];
455
+ for (let i = 1; i <= n; i++) {
456
+ const t = i / n;
457
+ out.push({ x: x0 + (x1 - x0) * t, y: y0 + (y1 - y0) * t });
458
+ }
459
+ return out;
460
+ }
461
+ function ellipseSamples(cx, cy, rx, ry, phi, segments) {
462
+ const cosPhi = Math.cos(phi);
463
+ const sinPhi = Math.sin(phi);
464
+ const out = [];
465
+ const n = Math.max(8, segments);
466
+ for (let i = 1; i <= n; i++) {
467
+ const ang = (i / n) * Math.PI * 2;
468
+ const lx = rx * Math.cos(ang);
469
+ const ly = ry * Math.sin(ang);
470
+ out.push({
471
+ x: cosPhi * lx - sinPhi * ly + cx,
472
+ y: sinPhi * lx + cosPhi * ly + cy,
473
+ });
474
+ }
475
+ return out;
476
+ }
477
+ function arcToPoints(x0, y0, rx, ry, rotDeg, largeArc, sweep, x1, y1, segments) {
478
+ const phi = (rotDeg * Math.PI) / 180;
479
+ if (!Number.isFinite(rx) || !Number.isFinite(ry) || Math.abs(rx) < 1e-6 || Math.abs(ry) < 1e-6) {
480
+ return lineSamples(x0, y0, x1, y1, Math.max(2, segments));
481
+ }
482
+ if (Math.hypot(x0 - x1, y0 - y1) < 1e-3) {
483
+ if (largeArc)
484
+ return ellipseSamples(x0, y0, Math.abs(rx), Math.abs(ry), phi, Math.max(8, segments));
485
+ return lineSamples(x0, y0, x1, y1, 2);
486
+ }
487
+ const center = arcCenter(x0, y0, rx, ry, phi, largeArc, sweep, x1, y1);
488
+ if (!center || !Number.isFinite(center.cx) || !Number.isFinite(center.cy) || !Number.isFinite(center.delta)) {
489
+ return lineSamples(x0, y0, x1, y1, Math.max(2, segments));
490
+ }
491
+ const { cx, cy, start, delta } = center;
492
+ const cosPhi = Math.cos(phi);
493
+ const sinPhi = Math.sin(phi);
494
+ const out = [];
495
+ for (let i = 1; i <= segments; i++) {
496
+ const t = i / segments;
497
+ const ang = start + delta * t;
498
+ const lx = rx * Math.cos(ang);
499
+ const ly = ry * Math.sin(ang);
500
+ out.push({
501
+ x: cosPhi * lx - sinPhi * ly + cx,
502
+ y: sinPhi * lx + cosPhi * ly + cy,
503
+ });
504
+ }
505
+ return out;
506
+ }
507
+ /** Sample absolute path commands into a polyline (Bezier / arc aware). */
508
+ export function flattenPathToPoints(commands, segmentsPerCurve = 8) {
509
+ const out = [];
510
+ let cx = 0;
511
+ let cy = 0;
512
+ let closed = false;
513
+ for (const { cmd, args } of commands) {
514
+ if (cmd === "Z") {
515
+ closed = true;
516
+ continue;
517
+ }
518
+ if (cmd === "M") {
519
+ cx = args[0];
520
+ cy = args[1];
521
+ out.push({ x: cx, y: cy });
522
+ continue;
523
+ }
524
+ if (cmd === "L") {
525
+ cx = args[0];
526
+ cy = args[1];
527
+ out.push({ x: cx, y: cy });
528
+ continue;
529
+ }
530
+ if (cmd === "C") {
531
+ const p0 = { x: cx, y: cy };
532
+ const p1 = { x: args[0], y: args[1] };
533
+ const p2 = { x: args[2], y: args[3] };
534
+ const p3 = { x: args[4], y: args[5] };
535
+ for (let i = 1; i <= segmentsPerCurve; i++) {
536
+ out.push(cubicAt(p0, p1, p2, p3, i / segmentsPerCurve));
537
+ }
538
+ cx = p3.x;
539
+ cy = p3.y;
540
+ continue;
541
+ }
542
+ if (cmd === "Q") {
543
+ const p0 = { x: cx, y: cy };
544
+ const p1 = { x: args[0], y: args[1] };
545
+ const p2 = { x: args[2], y: args[3] };
546
+ for (let i = 1; i <= segmentsPerCurve; i++) {
547
+ out.push(quadAt(p0, p1, p2, i / segmentsPerCurve));
548
+ }
549
+ cx = p2.x;
550
+ cy = p2.y;
551
+ continue;
552
+ }
553
+ if (cmd === "A") {
554
+ const pts = arcToPoints(cx, cy, args[0], args[1], args[2], args[3], args[4], args[5], args[6], Math.max(4, segmentsPerCurve));
555
+ out.push(...pts);
556
+ if (pts.length) {
557
+ cx = pts[pts.length - 1].x;
558
+ cy = pts[pts.length - 1].y;
559
+ }
560
+ continue;
561
+ }
562
+ }
563
+ if (closed && out.length > 1)
564
+ out.push({ ...out[0] });
565
+ return out;
566
+ }
567
+ function pathIsClosed(commands) {
568
+ return commands.some((c) => c.cmd === "Z");
569
+ }
570
+ function normalizePathD(d) {
571
+ return pathToAbsolute(parsePathCommands(d));
572
+ }
573
+ /** Build a tween for morphing `fromD` → `toD`, or null when only FLIP should apply. */
574
+ export function makeMorphPathTween(fromD, toD) {
575
+ const a = normalizePathD(fromD);
576
+ const b = normalizePathD(toD);
577
+ const na = commandsToNumbers(a);
578
+ const nb = commandsToNumbers(b);
579
+ if (na.cmds.join("") === nb.cmds.join("") && na.nums.length === nb.nums.length && na.nums.length > 0) {
580
+ return (t) => buildPathFromInterp(na.cmds, na.nums, nb.nums, t);
581
+ }
582
+ const polyA = asPolyline(a);
583
+ const polyB = asPolyline(b);
584
+ if (polyA && polyB) {
585
+ const closed = polyA.closed || polyB.closed;
586
+ const n = Math.max(polyA.points.length, polyB.points.length, closed ? 8 : 4);
587
+ const ra = resamplePolyline(polyA.points, polyA.closed, n);
588
+ const rb = alignResampledPolylines(ra, resamplePolyline(polyB.points, polyB.closed, n), closed);
589
+ return (t) => {
590
+ const mid = ra.map((p, i) => ({
591
+ x: lerp(p.x, rb[i].x, t),
592
+ y: lerp(p.y, rb[i].y, t),
593
+ }));
594
+ return polylinePath(mid, closed);
595
+ };
596
+ }
597
+ const flatA = flattenPathToPoints(a, 8);
598
+ const flatB = flattenPathToPoints(b, 8);
599
+ if (flatA.length >= 2 && flatB.length >= 2) {
600
+ const closed = pathIsClosed(a) || pathIsClosed(b);
601
+ const n = Math.max(flatA.length, flatB.length, 16);
602
+ const ra = resamplePolyline(flatA, closed, n);
603
+ const rb = alignResampledPolylines(ra, resamplePolyline(flatB, closed, n), closed);
604
+ return (t) => {
605
+ const mid = ra.map((p, i) => ({
606
+ x: lerp(p.x, rb[i].x, t),
607
+ y: lerp(p.y, rb[i].y, t),
608
+ }));
609
+ return polylinePath(mid, closed);
610
+ };
611
+ }
612
+ return null;
613
+ }
614
+ /** Whether path geometry can morph (vs FLIP-only). */
615
+ export function morphPathCompatible(fromD, toD) {
616
+ return makeMorphPathTween(fromD, toD) !== null;
617
+ }
618
+ /** Compare two paths after normalization (for shape matching). */
619
+ export function morphPathSignature(d) {
620
+ const cmds = normalizePathD(d);
621
+ const flat = flattenPathToPoints(cmds, 6);
622
+ if (flat.length >= 2) {
623
+ const closed = pathIsClosed(cmds);
624
+ const rs = resamplePolyline(flat, closed, 8);
625
+ return `flat|${closed ? 1 : 0}|${rs.map((p) => `${Math.round(p.x / 8)},${Math.round(p.y / 8)}`).join(";")}`;
626
+ }
627
+ const poly = asPolyline(cmds);
628
+ if (poly) {
629
+ const n = 6;
630
+ const rs = resamplePolyline(poly.points, poly.closed, n);
631
+ return `poly|${poly.closed ? 1 : 0}|${rs.map((p) => `${Math.round(p.x / 8)},${Math.round(p.y / 8)}`).join(";")}`;
632
+ }
633
+ const { cmds: c, nums } = commandsToNumbers(cmds);
634
+ const rounded = nums.map((v) => Math.round(v / 4));
635
+ return `${c.join("")}|${rounded.join(",")}`;
636
+ }