animot-presenter 0.6.3 → 0.6.5

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.
@@ -0,0 +1,420 @@
1
+ // flubber: import via NAMESPACE + default fallback, NOT named imports. Named
2
+ // imports (`import { interpolateAll } from 'flubber'`) resolve to flubber's ESM
3
+ // `src/` entry, whose `interpolateAll` breaks under Rollup's production bundle
4
+ // (it threw → makeMorphParts fell to its joined hard cut → multi-subpath morphs
5
+ // like the logo SNAPPED, while single-path morphs via `interpolate` were fine).
6
+ // The namespace+`.default ?? ns` form resolves to flubber's bundled CJS build
7
+ // where every function works — this is exactly what the editor app does, and it
8
+ // morphs correctly there. The "default is not exported" build warning is benign.
9
+ // Import the explicit CJS build, NOT the package root. The root's `module`
10
+ // (ESM) entry resolves under Rollup to flubber's `src/` files, whose
11
+ // `interpolateAll` breaks in the production bundle (multi-subpath morphs like
12
+ // the logo SNAP). The bundled CJS build has the working implementation.
13
+ // @ts-ignore — CJS build has no type declarations; used as `any` below.
14
+ import * as flubberNs from 'flubber/build/flubber.min.js';
15
+ const flubber = flubberNs.default ?? flubberNs;
16
+ const interpolate = flubber.interpolate;
17
+ const interpolateAll = flubber.interpolateAll;
18
+ const separate = flubber.separate;
19
+ const combine = flubber.combine;
20
+ const splitPathString = flubber.splitPathString;
21
+ import { parsePath, serializePath } from './svg-path-edit';
22
+ import { drawElementToPath } from './freehand';
23
+ /**
24
+ * Path morphing helpers shared by the editor preview and the presenter.
25
+ *
26
+ * The headline use is SVG→SVG (and shape→shape / shape→SVG) morphing across
27
+ * slides: extract a single path `d` from each endpoint, then build a flubber
28
+ * interpolator that produces an in-between `d` for any progress 0..1.
29
+ */
30
+ /** Ellipse as 4 cubic Béziers (kappa) — editable, unlike arc commands. */
31
+ function ellipsePath(cx, cy, rx, ry) {
32
+ const k = 0.5522847498307936;
33
+ const ox = rx * k, oy = ry * k;
34
+ return [
35
+ `M${cx - rx},${cy}`,
36
+ `C${cx - rx},${cy - oy} ${cx - ox},${cy - ry} ${cx},${cy - ry}`,
37
+ `C${cx + ox},${cy - ry} ${cx + rx},${cy - oy} ${cx + rx},${cy}`,
38
+ `C${cx + rx},${cy + oy} ${cx + ox},${cy + ry} ${cx},${cy + ry}`,
39
+ `C${cx - ox},${cy + ry} ${cx - rx},${cy + oy} ${cx - rx},${cy}`,
40
+ 'Z'
41
+ ].join(' ');
42
+ }
43
+ /** Build a `d` string for a shape type, fitted to a w×h box (origin 0,0). */
44
+ export function shapeToPath(type, w, h, borderRadius = 0) {
45
+ const r = Math.max(0, Math.min(borderRadius, Math.min(w, h) / 2));
46
+ switch (type) {
47
+ case 'rectangle': {
48
+ if (r <= 0)
49
+ return `M0,0 L${w},0 L${w},${h} L0,${h} Z`;
50
+ return `M${r},0 L${w - r},0 Q${w},0 ${w},${r} L${w},${h - r} Q${w},${h} ${w - r},${h} L${r},${h} Q0,${h} 0,${h - r} L0,${r} Q0,0 ${r},0 Z`;
51
+ }
52
+ case 'circle':
53
+ case 'ellipse':
54
+ return ellipsePath(w / 2, h / 2, w / 2, h / 2);
55
+ case 'triangle':
56
+ return `M${w / 2},0 L${w},${h} L0,${h} Z`;
57
+ case 'hexagon': {
58
+ // Pointy-left/right flat hexagon fitted to the box.
59
+ const q = w * 0.25;
60
+ return `M${q},0 L${w - q},0 L${w},${h / 2} L${w - q},${h} L${q},${h} L0,${h / 2} Z`;
61
+ }
62
+ case 'star': {
63
+ const cx = w / 2, cy = h / 2, outer = Math.min(w, h) / 2, inner = outer * 0.5;
64
+ let d = '';
65
+ for (let i = 0; i < 10; i++) {
66
+ const rad = i % 2 === 0 ? outer : inner;
67
+ const ang = -Math.PI / 2 + (i * Math.PI) / 5;
68
+ const x = cx + rad * Math.cos(ang);
69
+ const y = cy + rad * Math.sin(ang);
70
+ d += (i === 0 ? 'M' : 'L') + x.toFixed(2) + ',' + y.toFixed(2) + ' ';
71
+ }
72
+ return d + 'Z';
73
+ }
74
+ default:
75
+ return `M0,0 L${w},0 L${w},${h} L0,${h} Z`;
76
+ }
77
+ }
78
+ const NUM = '[-+]?[0-9]*\\.?[0-9]+(?:[eE][-+]?[0-9]+)?';
79
+ function nums(s) {
80
+ return (s.match(new RegExp(NUM, 'g')) ?? []).map(Number);
81
+ }
82
+ /**
83
+ * Convert a basic SVG primitive element to a path `d`. Returns '' for
84
+ * unsupported tags. Handles path/rect/circle/ellipse/line/polyline/polygon.
85
+ */
86
+ function primitiveToPath(tag, attrs) {
87
+ const attr = (name) => {
88
+ const m = attrs.match(new RegExp(name + '\\s*=\\s*["\']([^"\']*)["\']', 'i'));
89
+ return m ? m[1] : null;
90
+ };
91
+ switch (tag.toLowerCase()) {
92
+ case 'path':
93
+ return attr('d') ?? '';
94
+ case 'rect': {
95
+ const x = Number(attr('x') ?? 0), y = Number(attr('y') ?? 0);
96
+ const w = Number(attr('width') ?? 0), h = Number(attr('height') ?? 0);
97
+ const rx = Number(attr('rx') ?? 0);
98
+ if (rx > 0)
99
+ return `M${x + rx},${y} L${x + w - rx},${y} Q${x + w},${y} ${x + w},${y + rx} L${x + w},${y + h - rx} Q${x + w},${y + h} ${x + w - rx},${y + h} L${x + rx},${y + h} Q${x},${y + h} ${x},${y + h - rx} L${x},${y + rx} Q${x},${y} ${x + rx},${y} Z`;
100
+ return `M${x},${y} L${x + w},${y} L${x + w},${y + h} L${x},${y + h} Z`;
101
+ }
102
+ case 'circle': {
103
+ const cx = Number(attr('cx') ?? 0), cy = Number(attr('cy') ?? 0), rr = Number(attr('r') ?? 0);
104
+ return ellipsePath(cx, cy, rr, rr);
105
+ }
106
+ case 'ellipse': {
107
+ const cx = Number(attr('cx') ?? 0), cy = Number(attr('cy') ?? 0);
108
+ const rx = Number(attr('rx') ?? 0), ry = Number(attr('ry') ?? 0);
109
+ return ellipsePath(cx, cy, rx, ry);
110
+ }
111
+ case 'line': {
112
+ const [x1, y1, x2, y2] = [Number(attr('x1') ?? 0), Number(attr('y1') ?? 0), Number(attr('x2') ?? 0), Number(attr('y2') ?? 0)];
113
+ return `M${x1},${y1} L${x2},${y2}`;
114
+ }
115
+ case 'polyline':
116
+ case 'polygon': {
117
+ const pts = nums(attr('points') ?? '');
118
+ if (pts.length < 4)
119
+ return '';
120
+ let d = `M${pts[0]},${pts[1]}`;
121
+ for (let i = 2; i < pts.length; i += 2)
122
+ d += ` L${pts[i]},${pts[i + 1]}`;
123
+ return tag.toLowerCase() === 'polygon' ? d + ' Z' : d;
124
+ }
125
+ default:
126
+ return '';
127
+ }
128
+ }
129
+ /**
130
+ * Extract drawable path `d` strings from raw SVG markup. Returns an array of
131
+ * subpath `d` strings (one per primitive). Empty when nothing is drawable.
132
+ */
133
+ export function extractPaths(svgContent) {
134
+ const out = [];
135
+ const re = /<(path|rect|circle|ellipse|line|polyline|polygon)\b([^>]*)\/?>/gi;
136
+ let m;
137
+ while ((m = re.exec(svgContent)) !== null) {
138
+ const d = primitiveToPath(m[1], m[2]);
139
+ if (d.trim())
140
+ out.push(d.trim());
141
+ }
142
+ return out;
143
+ }
144
+ /** Resolve a single primitive's own fill (style="fill:…" or fill="…"). */
145
+ function primitiveFill(attrs) {
146
+ const style = attrs.match(/style\s*=\s*["']([^"']*)["']/i)?.[1];
147
+ if (style) {
148
+ const f = style.match(/fill\s*:\s*([^;]+)/i)?.[1]?.trim();
149
+ if (f && f.toLowerCase() !== 'none')
150
+ return f;
151
+ }
152
+ const attr = attrs.match(/\bfill\s*=\s*["']([^"']+)["']/i)?.[1]?.trim();
153
+ if (attr && attr.toLowerCase() !== 'none')
154
+ return attr;
155
+ return null;
156
+ }
157
+ /**
158
+ * Like extractPaths, but pairs each subpath with its own fill color so a
159
+ * multi-color SVG morphs per-piece (one `<path>` per subpath) instead of
160
+ * flattening to a single representative color. Primitives without their own
161
+ * fill inherit the root `<svg fill="…">` (default black, matching the
162
+ * browser), which is exactly the black outline strokes on icon SVGs.
163
+ */
164
+ export function extractPathsWithFill(svgContent) {
165
+ const rootFill = svgContent.match(/<svg\b[^>]*?\bfill\s*=\s*["']([^"']+)["']/i)?.[1] || '#000000';
166
+ const out = [];
167
+ const re = /<(path|rect|circle|ellipse|line|polyline|polygon)\b([^>]*)\/?>/gi;
168
+ let m;
169
+ while ((m = re.exec(svgContent)) !== null) {
170
+ const d = primitiveToPath(m[1], m[2]);
171
+ if (!d.trim())
172
+ continue;
173
+ out.push({ d: d.trim(), fill: primitiveFill(m[2]) ?? rootFill });
174
+ }
175
+ return out;
176
+ }
177
+ /** Single combined `d` (all subpaths concatenated) — for simple morphs. */
178
+ export function extractCombinedPath(svgContent) {
179
+ const paths = extractPaths(svgContent);
180
+ if (paths.length === 0)
181
+ return null;
182
+ return paths.join(' ');
183
+ }
184
+ /**
185
+ * Pick a representative fill color from SVG markup — the first non-"none"
186
+ * `fill` attribute. Used to color the flattened morph path (which loses
187
+ * per-subpath fills). Returns null if none found.
188
+ */
189
+ export function extractFill(svgContent) {
190
+ // Match both `fill="x"` attributes and `fill:x` inside `style="…"` —
191
+ // many real-world SVGs (e.g. svgrepo exports) put the color in a style
192
+ // declaration, which the attribute-only regex would miss (→ gray morph).
193
+ const re = /fill\s*[:=]\s*["']?\s*([^"';\s>]+)/gi;
194
+ let m;
195
+ while ((m = re.exec(svgContent)) !== null) {
196
+ const v = m[1].trim();
197
+ if (v && v.toLowerCase() !== 'none')
198
+ return v;
199
+ }
200
+ return null;
201
+ }
202
+ function parseViewBox(vb) {
203
+ const p = vb.split(/[\s,]+/).map(Number);
204
+ return [p[0] || 0, p[1] || 0, p[2] || 1, p[3] || 1];
205
+ }
206
+ /**
207
+ * Re-fit a path `d` from its own viewBox into a target W×H box (origin 0,0).
208
+ * Morph stops must share a coordinate space or flubber produces scale jumps;
209
+ * authoring normalizes every stop into the element's box via this. Parses to
210
+ * absolute cubics, scales anchors + handles, re-serializes.
211
+ */
212
+ export function fitPathToBox(d, sourceViewBox, targetW, targetH) {
213
+ const [vx, vy, vw, vh] = parseViewBox(sourceViewBox);
214
+ const sx = targetW / (vw || 1);
215
+ const sy = targetH / (vh || 1);
216
+ const subs = parsePath(d);
217
+ if (!subs)
218
+ return d;
219
+ for (const sub of subs) {
220
+ for (const a of sub.anchors) {
221
+ a.x = (a.x - vx) * sx;
222
+ a.y = (a.y - vy) * sy;
223
+ if (a.cin) {
224
+ a.cin.x = (a.cin.x - vx) * sx;
225
+ a.cin.y = (a.cin.y - vy) * sy;
226
+ }
227
+ if (a.cout) {
228
+ a.cout.x = (a.cout.x - vx) * sx;
229
+ a.cout.y = (a.cout.y - vy) * sy;
230
+ }
231
+ }
232
+ }
233
+ return serializePath(subs);
234
+ }
235
+ /**
236
+ * Resolve any vector element (shape / svg / draw) to its current geometry as
237
+ * a path `d` plus the viewBox that `d` lives in. Single source of truth for
238
+ * both the morph engine and the "snapshot current" authoring action.
239
+ */
240
+ export function resolveElementPath(el) {
241
+ if (el.type === 'shape') {
242
+ const s = el;
243
+ return { d: shapeToPath(s.shapeType, s.size.width, s.size.height, s.borderRadius), viewBox: `0 0 ${s.size.width} ${s.size.height}` };
244
+ }
245
+ if (el.type === 'svg') {
246
+ const s = el;
247
+ const d = extractCombinedPath(s.svgContent);
248
+ if (!d)
249
+ return null;
250
+ const vb = s.viewBox ?? s.svgContent.match(/viewBox=["']([^"']+)["']/i)?.[1] ?? `0 0 ${s.size.width} ${s.size.height}`;
251
+ return { d, viewBox: vb };
252
+ }
253
+ if (el.type === 'draw') {
254
+ const p = drawElementToPath(el);
255
+ return { d: p.d, viewBox: p.viewBox };
256
+ }
257
+ return null;
258
+ }
259
+ /**
260
+ * Like resolveElementPath but keeps subpaths SEPARATE, each with its own fill,
261
+ * so multi-color SVGs morph per-piece. Shapes/draws return a single part.
262
+ */
263
+ export function resolveElementPathParts(el) {
264
+ if (el.type === 'shape') {
265
+ const s = el;
266
+ return { parts: [{ d: shapeToPath(s.shapeType, s.size.width, s.size.height, s.borderRadius), fill: s.fillColor || '#888888' }], viewBox: `0 0 ${s.size.width} ${s.size.height}` };
267
+ }
268
+ if (el.type === 'svg') {
269
+ const s = el;
270
+ const parts = extractPathsWithFill(s.svgContent);
271
+ if (!parts.length)
272
+ return null;
273
+ const vb = s.viewBox ?? s.svgContent.match(/viewBox=["']([^"']+)["']/i)?.[1] ?? `0 0 ${s.size.width} ${s.size.height}`;
274
+ return { parts, viewBox: vb };
275
+ }
276
+ if (el.type === 'draw') {
277
+ const p = drawElementToPath(el);
278
+ return { parts: [{ d: p.d, fill: el.color }], viewBox: p.viewBox };
279
+ }
280
+ return null;
281
+ }
282
+ /**
283
+ * Build per-piece morph parts between two fill-tagged subpath lists. Each `d`
284
+ * MUST already be in the same coordinate space (caller fits the source into
285
+ * the target viewBox). Picks the right flubber routine by count so 1↔many and
286
+ * many↔1 map correctly, and pairs colors per piece so multi-color SVGs keep
287
+ * their colors instead of flattening to one.
288
+ */
289
+ export function makeMorphParts(from, to) {
290
+ const opts = { maxSegmentLength: 10 };
291
+ const fromN = from.length, toN = to.length;
292
+ if (fromN === 0 || toN === 0) {
293
+ const d = from[0]?.d || to[0]?.d || 'M0,0';
294
+ return [{ interp: () => d, fromColor: from[0]?.fill || '#888888', toColor: to[0]?.fill || '#888888' }];
295
+ }
296
+ // flubber's matched interpolators give the editor's "liquid" blend (the
297
+ // per-subpath index fallback below morphs piece-by-piece = rigid "lines").
298
+ // These work now that the UMD-context build fix repaired interpolateAll/
299
+ // separate/combine in the bundle; the per-subpath fallback covers any throw.
300
+ try {
301
+ if (fromN === 1 && toN === 1) {
302
+ return [{ interp: interpolate(from[0].d, to[0].d, opts), fromColor: from[0].fill, toColor: to[0].fill }];
303
+ }
304
+ if (fromN === 1 && toN > 1) {
305
+ const arr = separate(from[0].d, to.map((p) => p.d), opts);
306
+ return arr.map((fn, i) => ({ interp: fn, fromColor: from[0].fill, toColor: to[i % toN].fill }));
307
+ }
308
+ if (fromN > 1 && toN === 1) {
309
+ const arr = combine(from.map((p) => p.d), to[0].d, opts);
310
+ return arr.map((fn, i) => ({ interp: fn, fromColor: from[i % fromN].fill, toColor: to[0].fill }));
311
+ }
312
+ if (fromN === toN) {
313
+ const arr = interpolateAll(from.map((p) => p.d), to.map((p) => p.d), opts);
314
+ return arr.map((fn, i) => ({ interp: fn, fromColor: from[i % fromN].fill, toColor: to[i % toN].fill }));
315
+ }
316
+ const merged = from.map((p) => p.d).join(' ');
317
+ const arr = separate(merged, to.map((p) => p.d), opts);
318
+ return arr.map((fn, i) => ({ interp: fn, fromColor: from[0].fill, toColor: to[i % toN].fill }));
319
+ }
320
+ catch {
321
+ // Fallback: per-subpath index interpolation (rigid but never snaps).
322
+ const n = Math.max(fromN, toN);
323
+ const parts = [];
324
+ for (let i = 0; i < n; i++) {
325
+ const fp = from[Math.min(i, fromN - 1)];
326
+ const tp = to[Math.min(i, toN - 1)];
327
+ let interp;
328
+ try {
329
+ interp = interpolate(fp.d, tp.d, opts);
330
+ }
331
+ catch {
332
+ const a = fp.d, b = tp.d;
333
+ interp = (t) => (t < 0.5 ? a : b);
334
+ }
335
+ parts.push({ interp, fromColor: fp.fill, toColor: tp.fill });
336
+ }
337
+ return parts;
338
+ }
339
+ }
340
+ /**
341
+ * Build an interpolator between two `d` strings. Handles differing subpath
342
+ * counts via flubber's many-to-many interpolation, falling back to a simple
343
+ * single-path interpolate. Always returns a function (t:0..1) => d.
344
+ */
345
+ export function makePathInterpolator(fromD, toD) {
346
+ const opts = { maxSegmentLength: 10 };
347
+ let fromParts, toParts;
348
+ try {
349
+ fromParts = splitPathString(fromD);
350
+ toParts = splitPathString(toD);
351
+ }
352
+ catch {
353
+ fromParts = [fromD];
354
+ toParts = [toD];
355
+ }
356
+ if (fromParts.length === 0)
357
+ fromParts = [fromD];
358
+ if (toParts.length === 0)
359
+ toParts = [toD];
360
+ // Single subpath: plain `interpolate`.
361
+ if (fromParts.length === 1 && toParts.length === 1) {
362
+ try {
363
+ return interpolate(fromD, toD, opts);
364
+ }
365
+ catch {
366
+ return (t) => (t < 0.5 ? fromD : toD);
367
+ }
368
+ }
369
+ // Multi-subpath, EQUAL counts: use interpolateAll's ARRAY form (flubber's
370
+ // smart global matching) and concat manually. Falls back to per-subpath
371
+ // index interpolation for unequal counts or if interpolateAll still throws.
372
+ if (fromParts.length === toParts.length) {
373
+ try {
374
+ // `single: true` asks flubber to morph the multi-subpath shape as one
375
+ // unified path. This is the same behavior used by the app preview.
376
+ return interpolateAll(fromParts, toParts, { ...opts, single: true });
377
+ }
378
+ catch { /* fall through to per-subpath */ }
379
+ }
380
+ const n = Math.max(fromParts.length, toParts.length);
381
+ const interps = [];
382
+ for (let i = 0; i < n; i++) {
383
+ const a = fromParts[Math.min(i, fromParts.length - 1)];
384
+ const b = toParts[Math.min(i, toParts.length - 1)];
385
+ try {
386
+ interps.push(interpolate(a, b, opts));
387
+ }
388
+ catch {
389
+ interps.push((t) => (t < 0.5 ? a : b));
390
+ }
391
+ }
392
+ return (t) => interps.map((fn) => fn(t)).join(' ');
393
+ }
394
+ /**
395
+ * Build a single interpolator that morphs through an ordered list of `d`
396
+ * stops. `t` runs 0..1 across the whole sequence; the segment is picked by
397
+ * scaling `t` across (stops.length - 1) and interpolating within it. Used by
398
+ * the within-slide multi-target morph (circle → star → hexagon → …).
399
+ * When `loop` is true the sequence wraps back to the first stop.
400
+ */
401
+ export function makeSequenceInterpolator(stops, loop = false) {
402
+ const seq = loop && stops.length > 1 ? [...stops, stops[0]] : stops.slice();
403
+ if (seq.length === 0)
404
+ return () => '';
405
+ if (seq.length === 1)
406
+ return () => seq[0];
407
+ const segs = [];
408
+ for (let i = 0; i < seq.length - 1; i++)
409
+ segs.push(makePathInterpolator(seq[i], seq[i + 1]));
410
+ const n = segs.length;
411
+ return (t) => {
412
+ const clamped = Math.max(0, Math.min(1, t));
413
+ const scaled = clamped * n;
414
+ let idx = Math.floor(scaled);
415
+ if (idx >= n)
416
+ idx = n - 1;
417
+ const frac = scaled - idx;
418
+ return segs[idx](frac);
419
+ };
420
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Parse/serialize an SVG path `d` into an editable cubic-Bézier anchor model
3
+ * used by the node editor. Every segment is normalized to a cubic so each
4
+ * anchor has at most an incoming + outgoing control handle — the same mental
5
+ * model as the motion-path editor.
6
+ *
7
+ * Supported input commands: M m L l H h V v C c S s Q q T t Z z.
8
+ * Arcs (A/a) are not supported; `parsePath` returns null so the caller can
9
+ * keep node-editing disabled for those paths.
10
+ */
11
+ export interface Anchor {
12
+ x: number;
13
+ y: number;
14
+ /** Incoming control point (absolute). Undefined = straight into anchor. */
15
+ cin?: {
16
+ x: number;
17
+ y: number;
18
+ };
19
+ /** Outgoing control point (absolute). */
20
+ cout?: {
21
+ x: number;
22
+ y: number;
23
+ };
24
+ }
25
+ export interface SubPath {
26
+ anchors: Anchor[];
27
+ closed: boolean;
28
+ }
29
+ export declare function parsePath(d: string): SubPath[] | null;
30
+ /**
31
+ * Reduce anchor count via Ramer–Douglas–Peucker on anchor positions. Used to
32
+ * make a dense freehand outline node-editable (otherwise it has hundreds of
33
+ * handles). Kept anchors retain their Bézier handles; dropped ones become
34
+ * straight. `tolerance` is in the path's own coordinate units.
35
+ */
36
+ export declare function simplifyAnchors(subs: SubPath[], tolerance: number): SubPath[];
37
+ export declare function serializePath(subs: SubPath[]): string;