@glissade/lottie 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1175 @@
1
+ import { Group, ImageNode, Path, Rect, createScene } from "@glissade/scene";
2
+ import { cubicBezier, formatColor, sampleTrack, track } from "@glissade/core";
3
+ //#region src/spec.ts
4
+ var LottieImportError = class extends Error {
5
+ problems;
6
+ constructor(problems) {
7
+ super(`Lottie import rejected ${problems.length} unsupported feature(s):\n` + problems.map((p) => ` - ${p}`).join("\n"));
8
+ this.name = "LottieImportError";
9
+ this.problems = problems;
10
+ }
11
+ };
12
+ //#endregion
13
+ //#region src/keyframes.ts
14
+ /**
15
+ * Lottie property → glissade Key[] conversion: s/e keyframe normalization,
16
+ * the ease shift (Lottie eases live on the DEPARTING key; glissade EaseSpec
17
+ * lives on the ARRIVING key), hold keys, frame→seconds, and the 1 ms nudge
18
+ * for same-frame double keys.
19
+ */
20
+ const toSeconds = (tm, tFrames) => (tFrames + tm.st) / tm.fr + tm.offset;
21
+ const isKeyframed = (prop) => prop !== void 0 && Array.isArray(prop.k) && prop.k.length > 0 && typeof prop.k[0] === "object" && prop.k[0] !== null && !Array.isArray(prop.k[0]) && "t" in prop.k[0];
22
+ /** Resolve old-format s/e pairs (value at key j = s_j, falling back to e_{j-1}). */
23
+ function normalizeKeys(raw) {
24
+ const out = [];
25
+ for (let j = 0; j < raw.length; j++) {
26
+ const k = raw[j];
27
+ const prev = raw[j - 1];
28
+ const value = k.s ?? prev?.e ?? prev?.s;
29
+ out.push({
30
+ t: k.t,
31
+ value,
32
+ o: k.o ?? void 0,
33
+ i: k.i ?? void 0,
34
+ hold: k.h === 1,
35
+ to: k.to ?? void 0,
36
+ ti: k.ti ?? void 0
37
+ });
38
+ }
39
+ return out;
40
+ }
41
+ const handleAt = (h, dim, fallback) => {
42
+ if (h === void 0) return fallback;
43
+ if (Array.isArray(h)) return h[Math.min(dim, h.length - 1)] ?? fallback;
44
+ return h;
45
+ };
46
+ const clamp01 = (v) => Math.min(1, Math.max(0, v));
47
+ /**
48
+ * EaseSpec of the segment DEPARTING `from` (lands on the glissade arrival
49
+ * key). x handles are clamped to [0,1] (the time axis); y stays unclamped
50
+ * (overshoot is lossless). x≈y on both handles is the identity curve →
51
+ * undefined (linear).
52
+ */
53
+ function departingEase(from, dim = 0) {
54
+ if (!from.o || !from.i) return void 0;
55
+ const x1 = clamp01(handleAt(from.o.x, dim, 0));
56
+ const y1 = handleAt(from.o.y, dim, 0);
57
+ const x2 = clamp01(handleAt(from.i.x, dim, 1));
58
+ const y2 = handleAt(from.i.y, dim, 1);
59
+ if (Math.abs(x1 - y1) < 1e-9 && Math.abs(x2 - y2) < 1e-9) return void 0;
60
+ return {
61
+ kind: "cubicBezier",
62
+ pts: [
63
+ x1,
64
+ y1,
65
+ x2,
66
+ y2
67
+ ]
68
+ };
69
+ }
70
+ /** Per-dimension eases differ → vec2 tracks must split to component tracks. */
71
+ function easesDifferPerDim(norm, dims) {
72
+ for (const k of norm) for (const h of [k.o, k.i]) {
73
+ if (!h) continue;
74
+ for (const axis of [h.x, h.y]) {
75
+ if (!Array.isArray(axis) || axis.length < 2) continue;
76
+ const first = axis[0];
77
+ for (let d = 1; d < Math.min(dims, axis.length); d++) if (Math.abs(axis[d] - first) > 1e-9) return true;
78
+ }
79
+ }
80
+ return false;
81
+ }
82
+ /**
83
+ * Same-frame double keys (and any non-increasing t after rounding): nudge the
84
+ * later key 1 ms forward and make it a hold arrival, so the jump is preserved
85
+ * without violating validateTrack's strict ordering.
86
+ */
87
+ function enforceMonotonic(keys) {
88
+ for (let j = 1; j < keys.length; j++) {
89
+ const prev = keys[j - 1];
90
+ const cur = keys[j];
91
+ if (cur.t <= prev.t) {
92
+ cur.t = prev.t + .001;
93
+ cur.interp = "hold";
94
+ delete cur.ease;
95
+ }
96
+ }
97
+ return keys;
98
+ }
99
+ /**
100
+ * Generic ease-shifted conversion: glissade key j carries the ease/hold of
101
+ * Lottie key j−1's departing segment.
102
+ */
103
+ function convertKeys(norm, tm, map, dim = 0) {
104
+ const out = [];
105
+ for (let j = 0; j < norm.length; j++) {
106
+ const n = norm[j];
107
+ const k = {
108
+ t: toSeconds(tm, n.t),
109
+ value: map(n.value, n)
110
+ };
111
+ if (j > 0) {
112
+ const from = norm[j - 1];
113
+ if (from.hold) k.interp = "hold";
114
+ else {
115
+ const ease = departingEase(from, dim);
116
+ if (ease !== void 0) k.ease = ease;
117
+ }
118
+ }
119
+ out.push(k);
120
+ }
121
+ return enforceMonotonic(out);
122
+ }
123
+ const scalarOf = (v) => {
124
+ if (typeof v === "number") return v;
125
+ if (Array.isArray(v) && typeof v[0] === "number") return v[0];
126
+ throw new TypeError(`expected a Lottie scalar, got ${JSON.stringify(v)}`);
127
+ };
128
+ const vec2Of = (v) => {
129
+ if (Array.isArray(v) && typeof v[0] === "number" && typeof v[1] === "number") return [v[0], v[1]];
130
+ throw new TypeError(`expected a Lottie vector, got ${JSON.stringify(v)}`);
131
+ };
132
+ const SPATIAL_EPS = 1e-9;
133
+ const hasSpatialTangent = (k) => {
134
+ for (const t of [k.to, k.ti]) if (t && (Math.abs(t[0] ?? 0) > SPATIAL_EPS || Math.abs(t[1] ?? 0) > SPATIAL_EPS)) return true;
135
+ return false;
136
+ };
137
+ const cubicPoint = (p0, p1, p2, p3, u) => {
138
+ const w = 1 - u;
139
+ const a = w * w * w;
140
+ const b = 3 * w * w * u;
141
+ const c = 3 * w * u * u;
142
+ const d = u * u * u;
143
+ return [a * p0[0] + b * p1[0] + c * p2[0] + d * p3[0], a * p0[1] + b * p1[1] + c * p2[1] + d * p3[1]];
144
+ };
145
+ function arcLengthTable(p0, p1, p2, p3, samples = 128) {
146
+ const us = [0];
147
+ const lens = [0];
148
+ let prev = p0;
149
+ let acc = 0;
150
+ for (let s = 1; s <= samples; s++) {
151
+ const u = s / samples;
152
+ const pt = cubicPoint(p0, p1, p2, p3, u);
153
+ acc += Math.hypot(pt[0] - prev[0], pt[1] - prev[1]);
154
+ us.push(u);
155
+ lens.push(acc);
156
+ prev = pt;
157
+ }
158
+ return {
159
+ us,
160
+ lens,
161
+ total: acc
162
+ };
163
+ }
164
+ function uAtLength(table, target) {
165
+ const { us, lens } = table;
166
+ if (target <= 0) return 0;
167
+ if (target >= table.total) return 1;
168
+ let lo = 0;
169
+ let hi = lens.length - 1;
170
+ while (lo + 1 < hi) {
171
+ const mid = lo + hi >> 1;
172
+ if (lens[mid] <= target) lo = mid;
173
+ else hi = mid;
174
+ }
175
+ const span = lens[hi] - lens[lo];
176
+ const f = span > 0 ? (target - lens[lo]) / span : 0;
177
+ return us[lo] + f * (us[hi] - us[lo]);
178
+ }
179
+ /**
180
+ * Position keys with spatial ti/to: segments with tangents are BAKED to dense
181
+ * keys at the document fps by arc-length parameterization — Lottie maps the
182
+ * temporal ease onto distance along the curve, not the bezier parameter, so
183
+ * a parameter-space lerp would diverge mid-segment. Plain segments convert
184
+ * directly with their cubicBezier eases.
185
+ */
186
+ function convertPositionKeys(norm, tm, docFr) {
187
+ const out = [];
188
+ for (let j = 0; j < norm.length; j++) {
189
+ const n = norm[j];
190
+ const value = vec2Of(n.value);
191
+ const t = toSeconds(tm, n.t);
192
+ const from = norm[j - 1];
193
+ if (j === 0 || from.hold || !hasSpatialTangent(from)) {
194
+ const k = {
195
+ t,
196
+ value
197
+ };
198
+ if (j > 0) if (from.hold) k.interp = "hold";
199
+ else {
200
+ const ease = departingEase(from, 0);
201
+ if (ease !== void 0) k.ease = ease;
202
+ }
203
+ out.push(k);
204
+ continue;
205
+ }
206
+ const p0 = vec2Of(from.value);
207
+ const p3 = value;
208
+ const p1 = [p0[0] + (from.to?.[0] ?? 0), p0[1] + (from.to?.[1] ?? 0)];
209
+ const p2 = [p3[0] + (from.ti?.[0] ?? 0), p3[1] + (from.ti?.[1] ?? 0)];
210
+ const table = arcLengthTable(p0, p1, p2, p3);
211
+ const easeSpec = departingEase(from, 0);
212
+ const easeFn = easeSpec !== void 0 && typeof easeSpec === "object" && easeSpec.kind === "cubicBezier" ? cubicBezier(...easeSpec.pts) : (u) => u;
213
+ const t0 = out[out.length - 1].t;
214
+ const step = 1 / docFr;
215
+ for (let s = 1; t0 + s * step < t - step / 2; s++) {
216
+ const bt = t0 + s * step;
217
+ const u = uAtLength(table, easeFn((bt - t0) / (t - t0)) * table.total);
218
+ out.push({
219
+ t: bt,
220
+ value: cubicPoint(p0, p1, p2, p3, u)
221
+ });
222
+ }
223
+ out.push({
224
+ t,
225
+ value
226
+ });
227
+ }
228
+ return enforceMonotonic(out);
229
+ }
230
+ //#endregion
231
+ //#region src/audit.ts
232
+ /**
233
+ * Fail-fast feature audit (lottie-import.md §1): ONE pass over the document
234
+ * collecting EVERY unsupported feature, so users see all rejections at once.
235
+ * `allowDegraded` downgrades the defined degradable subset (expressions,
236
+ * merge-paths modes ≠ 1) to warnings; degradations are never silent.
237
+ */
238
+ const LAYER_TYPE_NAMES = {
239
+ 0: "precomp",
240
+ 5: "text",
241
+ 6: "audio",
242
+ 7: "pholderVideo",
243
+ 8: "imageSeq",
244
+ 9: "video",
245
+ 10: "pholderStill",
246
+ 13: "camera"
247
+ };
248
+ const SUPPORTED_LAYER_TYPES = new Set([
249
+ 1,
250
+ 2,
251
+ 3,
252
+ 4
253
+ ]);
254
+ function reject(ctx, errorClass, detail) {
255
+ ctx.problems.push(`[${errorClass}] ${detail}`);
256
+ }
257
+ function degrade(ctx, errorClass, detail) {
258
+ if (ctx.allowDegraded) ctx.warnings.push(`[${errorClass}] ${detail} — skipped (degraded)`);
259
+ else ctx.problems.push(`[${errorClass}] ${detail} (degradable: re-run with allowDegraded to warn and skip)`);
260
+ }
261
+ const isProp = (v) => typeof v === "object" && v !== null && ("k" in v || "a" in v || "x" in v);
262
+ function checkExpression(ctx, prop, where) {
263
+ if (isProp(prop) && typeof prop.x === "string" && prop.x.length > 0) degrade(ctx, "unsupported-expression", `expression on ${where}`);
264
+ }
265
+ /** Non-zero (or animated) skew is unrepresentable in fromTRS. */
266
+ function checkSkew(ctx, tr, where) {
267
+ for (const [name, prop] of [["sk", tr.sk], ["sa", tr.sa]]) {
268
+ if (prop === void 0) continue;
269
+ if (isKeyframed(prop)) {
270
+ if (normalizeKeys(prop.k).map((k) => scalarOf(k.value)).some((v) => Math.abs(v) > 1e-9)) reject(ctx, "unsupported-transform", `animated skew (${name}) on ${where}`);
271
+ } else if (prop.k !== void 0 && Math.abs(scalarOf(prop.k)) > 1e-9) reject(ctx, "unsupported-transform", `skew (${name}) on ${where}`);
272
+ }
273
+ }
274
+ function checkTransformExpressions(ctx, tr, where) {
275
+ const p = tr.p;
276
+ if (p && p.s === true) {
277
+ checkExpression(ctx, p.x, `${where}.p.x`);
278
+ checkExpression(ctx, p.y, `${where}.p.y`);
279
+ } else checkExpression(ctx, p, `${where}.p`);
280
+ checkExpression(ctx, tr.a, `${where}.a`);
281
+ checkExpression(ctx, tr.s, `${where}.s`);
282
+ checkExpression(ctx, tr.r, `${where}.r`);
283
+ checkExpression(ctx, tr.o, `${where}.o`);
284
+ }
285
+ const UNSUPPORTED_SHAPE_ITEMS = {
286
+ tm: "trim paths",
287
+ rp: "repeater",
288
+ rd: "round corners",
289
+ sr: "polystar",
290
+ gf: "gradient fill",
291
+ gs: "gradient stroke",
292
+ zz: "zig-zag",
293
+ op: "offset path",
294
+ pb: "pucker & bloat",
295
+ tw: "twist"
296
+ };
297
+ function auditShapeItems(ctx, items, where) {
298
+ let sawGroup = false;
299
+ for (let i = 0; i < items.length; i++) {
300
+ const item = items[i];
301
+ if (item.hd === true) continue;
302
+ const here = `${where}/${item.nm ?? `${item.ty}[${i}]`}`;
303
+ if (item.ty === "st" && item.d !== void 0) reject(ctx, "unsupported-feature", `stroke dashes at ${here}`);
304
+ const unsupported = UNSUPPORTED_SHAPE_ITEMS[item.ty];
305
+ if (unsupported) {
306
+ reject(ctx, "unsupported-shape-item", `${unsupported} at ${here}`);
307
+ continue;
308
+ }
309
+ switch (item.ty) {
310
+ case "gr": {
311
+ sawGroup = true;
312
+ const inner = item.it ?? [];
313
+ const tr = inner.find((it) => it.ty === "tr");
314
+ if (tr) {
315
+ checkSkew(ctx, tr, here);
316
+ checkTransformExpressions(ctx, tr, here);
317
+ }
318
+ auditShapeItems(ctx, inner.filter((it) => it.ty !== "tr"), here);
319
+ break;
320
+ }
321
+ case "sh":
322
+ checkExpression(ctx, item.ks, here);
323
+ break;
324
+ case "el":
325
+ case "rc":
326
+ checkExpression(ctx, item.p, `${here}.p`);
327
+ checkExpression(ctx, item.s, `${here}.s`);
328
+ checkExpression(ctx, item.r, `${here}.r`);
329
+ break;
330
+ case "fl":
331
+ case "st":
332
+ if (item.ty === "fl" && typeof item.r === "number" && item.r === 2) reject(ctx, "unsupported-fill-rule", `even-odd fill at ${here}`);
333
+ if (sawGroup) reject(ctx, "unsupported-shape-structure", `style inheriting into a preceding group at ${here}`);
334
+ checkExpression(ctx, item.c, `${here}.c`);
335
+ checkExpression(ctx, item.o, `${here}.o`);
336
+ checkExpression(ctx, item.w, `${here}.w`);
337
+ break;
338
+ case "mm": {
339
+ const mode = item.mm ?? 1;
340
+ if (mode !== 1) degrade(ctx, "unsupported-shape-modifier", `merge paths mode ${mode} at ${here}`);
341
+ break;
342
+ }
343
+ default: break;
344
+ }
345
+ }
346
+ }
347
+ function auditLayer(ctx, layer, index, doc) {
348
+ if (layer.hd === true) return;
349
+ const where = `layer ${layer.ind ?? index} '${layer.nm ?? "?"}'`;
350
+ if (!SUPPORTED_LAYER_TYPES.has(layer.ty)) {
351
+ reject(ctx, "unsupported-layer-type", `${LAYER_TYPE_NAMES[layer.ty] ?? `ty:${layer.ty}`} ${where}`);
352
+ return;
353
+ }
354
+ if (layer.ddd === 1) reject(ctx, "unsupported-feature", `3D layer ${where}`);
355
+ if ((layer.masksProperties?.length ?? 0) > 0 || layer.hasMask === true) reject(ctx, "unsupported-masking", `masks on ${where}`);
356
+ if (layer.tt !== void 0 && layer.tt !== 0) reject(ctx, "unsupported-masking", `track matte (tt:${layer.tt}) on ${where}`);
357
+ if (layer.td !== void 0 && layer.td !== 0) reject(ctx, "unsupported-masking", `matte source (td:${layer.td}) ${where}`);
358
+ if (layer.tm !== void 0) reject(ctx, "unsupported-time-remap", `time remap on ${where}`);
359
+ if ((layer.ef?.length ?? 0) > 0) reject(ctx, "unsupported-feature", `effects on ${where}`);
360
+ if ((layer.sy?.length ?? 0) > 0) reject(ctx, "unsupported-feature", `layer styles on ${where}`);
361
+ if (layer.sr !== void 0 && layer.sr !== 1) reject(ctx, "unsupported-time-remap", `layer time stretch (sr:${layer.sr}) on ${where}`);
362
+ if (layer.ao === 1) reject(ctx, "unsupported-transform", `auto-orient on ${where}`);
363
+ if (layer.ks) {
364
+ checkSkew(ctx, layer.ks, where);
365
+ checkTransformExpressions(ctx, layer.ks, where);
366
+ }
367
+ if (layer.ty === 2) {
368
+ const asset = (doc.assets ?? []).find((a) => a.id === layer.refId);
369
+ if (!asset || typeof asset.p !== "string") reject(ctx, "invalid-asset", `image ${where} references missing asset '${layer.refId ?? ""}'`);
370
+ }
371
+ if (layer.shapes) auditShapeItems(ctx, layer.shapes, where);
372
+ }
373
+ /** Throws LottieImportError listing EVERY rejection; returns collected warnings. */
374
+ function auditDocument(doc, allowDegraded) {
375
+ const ctx = {
376
+ problems: [],
377
+ warnings: [],
378
+ allowDegraded
379
+ };
380
+ if (doc.ddd === 1) reject(ctx, "unsupported-feature", "3D document");
381
+ doc.layers.forEach((layer, i) => auditLayer(ctx, layer, i, doc));
382
+ if (ctx.problems.length > 0) throw new LottieImportError(ctx.problems);
383
+ return { warnings: ctx.warnings };
384
+ }
385
+ //#endregion
386
+ //#region src/build.ts
387
+ /**
388
+ * Spec tree → real @glissade/scene nodes. Kept separate from conversion so
389
+ * importLottie's data output stays plain JSON-able.
390
+ */
391
+ function buildNode(spec) {
392
+ const base = {
393
+ id: spec.id,
394
+ ...spec.position !== void 0 ? { position: spec.position } : {},
395
+ ...spec.rotation !== void 0 ? { rotation: spec.rotation } : {},
396
+ ...spec.scale !== void 0 ? { scale: spec.scale } : {},
397
+ ...spec.opacity !== void 0 ? { opacity: spec.opacity } : {},
398
+ ...spec.zIndex !== void 0 ? { zIndex: spec.zIndex } : {}
399
+ };
400
+ switch (spec.kind) {
401
+ case "group": return new Group({
402
+ ...base,
403
+ children: spec.children.map(buildNode)
404
+ });
405
+ case "path": return new Path({
406
+ ...base,
407
+ data: spec.data,
408
+ ...spec.fill !== void 0 ? { fill: spec.fill } : {},
409
+ ...spec.stroke !== void 0 ? { stroke: spec.stroke } : {},
410
+ ...spec.strokeWidth !== void 0 ? { strokeWidth: spec.strokeWidth } : {}
411
+ });
412
+ case "rect": return new Rect({
413
+ ...base,
414
+ width: spec.width,
415
+ height: spec.height,
416
+ ...spec.fill !== void 0 ? { fill: spec.fill } : {}
417
+ });
418
+ case "image": return new ImageNode({
419
+ ...base,
420
+ assetId: spec.assetId,
421
+ width: spec.width,
422
+ height: spec.height
423
+ });
424
+ }
425
+ }
426
+ function buildNodes(specs) {
427
+ return specs.map(buildNode);
428
+ }
429
+ //#endregion
430
+ //#region src/pathvalue.ts
431
+ const v2 = (p) => [p?.[0] ?? 0, p?.[1] ?? 0];
432
+ /** sh data → one contour. `closedFallback` covers the old top-level `closed` flag. */
433
+ function shToContour(data, closedFallback) {
434
+ return {
435
+ closed: data.c ?? closedFallback ?? false,
436
+ v: data.v.map(v2),
437
+ in: data.i.map(v2),
438
+ out: data.o.map(v2)
439
+ };
440
+ }
441
+ /**
442
+ * Bezier circle constant used by AE/lottie-web for ellipses and round rect
443
+ * corners. The resulting control points are LINEAR in the radius, so lerping
444
+ * converted contours equals converting lerped sizes (exact per-key import).
445
+ */
446
+ const KAPPA = .5519;
447
+ /** el (ellipse): center p, size s → 4-vertex kappa-form contour. */
448
+ function ellipseContour(center, size) {
449
+ const rx = size[0] / 2;
450
+ const ry = size[1] / 2;
451
+ const [cx, cy] = center;
452
+ const kx = KAPPA * rx;
453
+ const ky = KAPPA * ry;
454
+ return {
455
+ closed: true,
456
+ v: [
457
+ [cx, cy - ry],
458
+ [cx + rx, cy],
459
+ [cx, cy + ry],
460
+ [cx - rx, cy]
461
+ ],
462
+ in: [
463
+ [-kx, 0],
464
+ [0, -ky],
465
+ [kx, 0],
466
+ [0, ky]
467
+ ],
468
+ out: [
469
+ [kx, 0],
470
+ [0, ky],
471
+ [-kx, 0],
472
+ [0, -ky]
473
+ ]
474
+ };
475
+ }
476
+ /** rc (rectangle): center p, size s, corner radius r → contour (kappa corners). */
477
+ function rectContour(center, size, radius) {
478
+ const w = size[0] / 2;
479
+ const h = size[1] / 2;
480
+ const [cx, cy] = center;
481
+ const r = Math.min(Math.max(0, radius), w, h);
482
+ if (r <= 0) {
483
+ const z = [0, 0];
484
+ return {
485
+ closed: true,
486
+ v: [
487
+ [cx + w, cy - h],
488
+ [cx + w, cy + h],
489
+ [cx - w, cy + h],
490
+ [cx - w, cy - h]
491
+ ],
492
+ in: [
493
+ z,
494
+ z,
495
+ z,
496
+ z
497
+ ],
498
+ out: [
499
+ z,
500
+ z,
501
+ z,
502
+ z
503
+ ]
504
+ };
505
+ }
506
+ const k = KAPPA * r;
507
+ const z = [0, 0];
508
+ return {
509
+ closed: true,
510
+ v: [
511
+ [cx + w - r, cy - h],
512
+ [cx + w, cy - h + r],
513
+ [cx + w, cy + h - r],
514
+ [cx + w - r, cy + h],
515
+ [cx - w + r, cy + h],
516
+ [cx - w, cy + h - r],
517
+ [cx - w, cy - h + r],
518
+ [cx - w + r, cy - h]
519
+ ],
520
+ in: [
521
+ z,
522
+ [0, -k],
523
+ z,
524
+ [k, 0],
525
+ z,
526
+ [0, k],
527
+ z,
528
+ [-k, 0]
529
+ ],
530
+ out: [
531
+ [k, 0],
532
+ z,
533
+ [0, k],
534
+ z,
535
+ [-k, 0],
536
+ z,
537
+ [0, -k],
538
+ z
539
+ ]
540
+ };
541
+ }
542
+ /**
543
+ * Reverse a contour's winding (Lottie shape direction d:3): reversed vertex
544
+ * order with in/out tangents exchanged. Winding decides nonzero-merge holes.
545
+ */
546
+ function reverseContour(c) {
547
+ const idx = c.v.map((_, i) => i).reverse();
548
+ return {
549
+ closed: c.closed,
550
+ v: idx.map((i) => c.v[i]),
551
+ in: idx.map((i) => c.out[i]),
552
+ out: idx.map((i) => c.in[i])
553
+ };
554
+ }
555
+ /** mm mode 1 (merge): plain contour concatenation into one multi-contour value. */
556
+ function mergeContours(values) {
557
+ return values.flatMap((v) => v);
558
+ }
559
+ //#endregion
560
+ //#region src/convert.ts
561
+ /**
562
+ * Document conversion (lottie-import.md §3 Stage 1): layers → constructor
563
+ * specs + Timeline tracks. Structural rules: the anchor sandwich (outer node
564
+ * carries p/r/s, inner child offset by −a), parent chains nest into the
565
+ * parent's anchor group, layer opacity lives on a CONTENT sibling (Lottie
566
+ * parenting never inherits opacity), ip/op become hold-key opacity wrappers,
567
+ * and Lottie's top-layer-first stacking maps to zIndex = −ind.
568
+ */
569
+ function uid(ctx, base) {
570
+ const clean = base.replace(/[^A-Za-z0-9_]/g, "_") || "node";
571
+ let id = clean;
572
+ for (let n = 2; ctx.ids.has(id); n++) id = `${clean}_${n}`;
573
+ ctx.ids.add(id);
574
+ return id;
575
+ }
576
+ const norms = (prop) => isKeyframed(prop) ? normalizeKeys(prop.k) : void 0;
577
+ /**
578
+ * Lottie color array → hex. The byte-vs-float format is a property of the
579
+ * EXPORTER (whole prop), never of one key: a [1,1,1] key inside a byte-format
580
+ * track is rgb(1,1,1) near-black, not white — so callers classify once over
581
+ * every value of the prop and pass `bytes` in.
582
+ */
583
+ function lottieColor(value, bytes) {
584
+ const arr = value;
585
+ const ch = (i) => bytes ? arr[i] ?? 0 : (arr[i] ?? 0) * 255;
586
+ const a = arr.length > 3 ? bytes ? (arr[3] ?? 255) / 255 : arr[3] ?? 1 : 1;
587
+ return formatColor({
588
+ r: ch(0),
589
+ g: ch(1),
590
+ b: ch(2),
591
+ a
592
+ });
593
+ }
594
+ /** True when ANY component across the prop's values exceeds 1 (old byte exports). */
595
+ function colorPropIsBytes(values) {
596
+ return values.some((v) => Array.isArray(v) && v.some((c) => c > 1));
597
+ }
598
+ function pushTrack(ctx, target, type, keys) {
599
+ if (keys.length === 0) return;
600
+ ctx.tracks.push(track(target, type, keys));
601
+ }
602
+ const isSplitPosition = (p) => p !== void 0 && p.s === true;
603
+ function applyScalarProp(ctx, spec, prop, targetPath, map, assign, tm) {
604
+ if (prop === void 0) return;
605
+ const n = norms(prop);
606
+ if (n) {
607
+ const keys = convertKeys(n, tm, (v) => map(scalarOf(v)));
608
+ assign(keys[0].value);
609
+ pushTrack(ctx, `${spec.id}/${targetPath}`, "number", keys);
610
+ } else if (prop.k !== void 0) assign(map(scalarOf(prop.k)));
611
+ }
612
+ function applyVecProp(ctx, spec, prop, targetPath, map, assign, tm) {
613
+ if (prop === void 0) return;
614
+ const n = norms(prop);
615
+ if (n) if (easesDifferPerDim(n, 2)) {
616
+ for (const dim of [0, 1]) {
617
+ const keys = convertKeys(n, tm, (v) => map(vec2Of(v))[dim], dim);
618
+ pushTrack(ctx, `${spec.id}/${targetPath}.${dim === 0 ? "x" : "y"}`, "number", keys);
619
+ }
620
+ assign(map(vec2Of(n[0].value)));
621
+ } else {
622
+ const keys = convertKeys(n, tm, (v) => map(vec2Of(v)));
623
+ assign(keys[0].value);
624
+ pushTrack(ctx, `${spec.id}/${targetPath}`, "vec2", keys);
625
+ }
626
+ else if (prop.k !== void 0) assign(map(vec2Of(prop.k)));
627
+ }
628
+ function applyPosition(ctx, spec, p, tm) {
629
+ if (p === void 0) return;
630
+ if (isSplitPosition(p)) {
631
+ const pos = [0, 0];
632
+ for (const [dim, axis, prop] of [[
633
+ 0,
634
+ "x",
635
+ p.x
636
+ ], [
637
+ 1,
638
+ "y",
639
+ p.y
640
+ ]]) {
641
+ const n = norms(prop);
642
+ if (n) {
643
+ const keys = convertKeys(n, tm, (v) => scalarOf(v));
644
+ pos[dim] = keys[0].value;
645
+ pushTrack(ctx, `${spec.id}/position.${axis}`, "number", keys);
646
+ } else if (prop.k !== void 0) pos[dim] = scalarOf(prop.k);
647
+ }
648
+ spec.position = pos;
649
+ return;
650
+ }
651
+ const n = norms(p);
652
+ if (n) if (!n.some((k) => [k.to, k.ti].some((t) => t && (Math.abs(t[0] ?? 0) > 1e-9 || Math.abs(t[1] ?? 0) > 1e-9))) && easesDifferPerDim(n, 2)) {
653
+ for (const [dim, axis] of [[0, "x"], [1, "y"]]) {
654
+ const keys = convertKeys(n, tm, (v) => vec2Of(v)[dim], dim);
655
+ pushTrack(ctx, `${spec.id}/position.${axis}`, "number", keys);
656
+ }
657
+ spec.position = vec2Of(n[0].value);
658
+ } else {
659
+ const keys = convertPositionKeys(n, tm, ctx.doc.fr);
660
+ spec.position = keys[0].value;
661
+ pushTrack(ctx, `${spec.id}/position`, "vec2", keys);
662
+ }
663
+ else if (p.k !== void 0) spec.position = vec2Of(p.k);
664
+ }
665
+ /**
666
+ * Anchor sandwich: returns the inner group (offset by −a) when the anchor is
667
+ * non-zero or animated; otherwise content attaches to the outer group
668
+ * directly. Negation commutes with lerp, so animated anchors stay exact.
669
+ */
670
+ function applyAnchor(ctx, outer, a, tm) {
671
+ const makeInner = () => {
672
+ const inner = {
673
+ kind: "group",
674
+ id: uid(ctx, `${outer.id}__a`),
675
+ children: []
676
+ };
677
+ outer.children.push(inner);
678
+ return inner;
679
+ };
680
+ const n = norms(a);
681
+ if (n) {
682
+ const negated = n.map((k) => ({
683
+ ...k,
684
+ value: k.value.map((c) => -c),
685
+ to: k.to?.map((c) => -c),
686
+ ti: k.ti?.map((c) => -c)
687
+ }));
688
+ const inner = makeInner();
689
+ const keys = convertPositionKeys(negated, tm, ctx.doc.fr);
690
+ inner.position = keys[0].value;
691
+ pushTrack(ctx, `${inner.id}/position`, "vec2", keys);
692
+ return inner;
693
+ }
694
+ if (a?.k !== void 0) {
695
+ const [ax, ay] = vec2Of(a.k);
696
+ if (ax !== 0 || ay !== 0) {
697
+ const inner = makeInner();
698
+ inner.position = [-ax, -ay];
699
+ return inner;
700
+ }
701
+ }
702
+ return outer;
703
+ }
704
+ /** p/r/s onto `spec`; returns the attach point (anchor inner group or spec). */
705
+ function applyPRS(ctx, spec, ks, tm) {
706
+ applyPosition(ctx, spec, ks.p, tm);
707
+ applyScalarProp(ctx, spec, ks.r, "rotation", (v) => v, (v) => spec.rotation = v, tm);
708
+ applyVecProp(ctx, spec, ks.s, "scale", (v) => [v[0] / 100, v[1] / 100], (v) => spec.scale = v, tm);
709
+ return applyAnchor(ctx, spec, ks.a, tm);
710
+ }
711
+ function applyOpacity(ctx, spec, o, tm) {
712
+ applyScalarProp(ctx, spec, o, "opacity", (v) => v / 100, (v) => spec.opacity = v, tm);
713
+ }
714
+ const aligned = (a, b) => a.length === b.length && a.every((k, j) => k.t === b[j].t);
715
+ /** el/rc: animated parameters convert per-key (exact when key grids align). */
716
+ function combineParametric(ctx, props, build, tm, where) {
717
+ const animated = props.filter((p) => p.norm !== void 0);
718
+ const valuesAt = (pick) => props.map(pick);
719
+ if (animated.length === 0) return {
720
+ kind: "static",
721
+ value: build(valuesAt((p) => p.map(p.staticVal)))
722
+ };
723
+ const first = animated[0].norm;
724
+ if (animated.every((p) => aligned(p.norm, first))) {
725
+ if (!animated.every((p) => p.norm.every((k, j) => k.hold === first[j].hold && JSON.stringify(k.o ?? null) === JSON.stringify(first[j].o ?? null) && JSON.stringify(k.i ?? null) === JSON.stringify(first[j].i ?? null)))) ctx.warnings.push(`[approximation] ${where}: co-keyed parameters with differing eases use the first parameter's ease (endpoints exact, mid-segment may differ)`);
726
+ const keys = convertKeys(first, tm, (_v, _n) => []);
727
+ for (let j = 0; j < keys.length; j++) keys[j].value = build(valuesAt((p) => p.map(p.norm !== void 0 ? p.norm[j].value : p.staticVal)));
728
+ return {
729
+ kind: "animated",
730
+ keys
731
+ };
732
+ }
733
+ ctx.warnings.push(`[approximation] ${where}: independently-keyed parameters baked densely at ${ctx.doc.fr} fps`);
734
+ const samplers = props.map((p) => {
735
+ if (p.norm === void 0) return () => p.map(p.staticVal);
736
+ const keys = p.type === "vec2" ? convertKeys(p.norm, tm, (v) => vec2Of(v)) : convertKeys(p.norm, tm, (v) => scalarOf(v));
737
+ const tr = track(`__bake/${p.type}`, p.type, keys);
738
+ return (t) => p.map(sampleTrack(tr, t));
739
+ });
740
+ const times = animated.flatMap((p) => p.norm.map((k) => toSeconds(tm, k.t)));
741
+ const t0 = Math.min(...times);
742
+ const t1 = Math.max(...times);
743
+ const step = 1 / ctx.doc.fr;
744
+ const keys = [];
745
+ for (let t = t0; t < t1 + step / 2; t += step) keys.push({
746
+ t: Math.min(t, t1),
747
+ value: build(samplers.map((s) => s(Math.min(t, t1))))
748
+ });
749
+ return {
750
+ kind: "animated",
751
+ keys: enforceMonotonic(keys)
752
+ };
753
+ }
754
+ function geometrySource(ctx, item, tm, where) {
755
+ switch (item.ty) {
756
+ case "sh": {
757
+ const toValue = (v) => (Array.isArray(v) ? v : [v]).map((d) => shToContour(d, item.closed));
758
+ const n = norms(item.ks);
759
+ if (n) return {
760
+ kind: "animated",
761
+ keys: convertKeys(n, tm, toValue)
762
+ };
763
+ const k = item.ks?.k;
764
+ if (k === void 0) return null;
765
+ return {
766
+ kind: "static",
767
+ value: toValue(k)
768
+ };
769
+ }
770
+ case "el": {
771
+ const p = {
772
+ norm: norms(item.p),
773
+ staticVal: item.p?.k ?? [0, 0],
774
+ type: "vec2",
775
+ map: (v) => vec2Of(v)
776
+ };
777
+ const s = {
778
+ norm: norms(item.s),
779
+ staticVal: item.s?.k ?? [0, 0],
780
+ type: "vec2",
781
+ map: (v) => vec2Of(v)
782
+ };
783
+ const reversed = item.d === 3;
784
+ return combineParametric(ctx, [p, s], (vals) => {
785
+ const c = ellipseContour(vals[0], vals[1]);
786
+ return [reversed ? reverseContour(c) : c];
787
+ }, tm, where);
788
+ }
789
+ case "rc": {
790
+ const rProp = typeof item.r === "number" ? { k: item.r } : item.r;
791
+ const p = {
792
+ norm: norms(item.p),
793
+ staticVal: item.p?.k ?? [0, 0],
794
+ type: "vec2",
795
+ map: (v) => vec2Of(v)
796
+ };
797
+ const s = {
798
+ norm: norms(item.s),
799
+ staticVal: item.s?.k ?? [0, 0],
800
+ type: "vec2",
801
+ map: (v) => vec2Of(v)
802
+ };
803
+ const r = {
804
+ norm: norms(rProp),
805
+ staticVal: rProp?.k ?? 0,
806
+ type: "number",
807
+ map: (v) => scalarOf(v)
808
+ };
809
+ const reversed = item.d === 3;
810
+ return combineParametric(ctx, [
811
+ p,
812
+ s,
813
+ r
814
+ ], (vals) => {
815
+ const c = rectContour(vals[0], vals[1], vals[2]);
816
+ return [reversed ? reverseContour(c) : c];
817
+ }, tm, where);
818
+ }
819
+ default: return null;
820
+ }
821
+ }
822
+ /** mm mode 1: concatenate contours into one multi-contour source. */
823
+ function mergeGeomSources(ctx, sources, tm, where) {
824
+ if (sources.length === 1) return sources[0];
825
+ const animated = sources.filter((s) => s.kind === "animated");
826
+ if (animated.length === 0) return {
827
+ kind: "static",
828
+ value: mergeContours(sources.map((s) => s.value))
829
+ };
830
+ const first = animated[0].keys;
831
+ if (animated.every((s) => s.keys.length === first.length && s.keys.every((k, j) => k.t === first[j].t))) return {
832
+ kind: "animated",
833
+ keys: first.map((k, j) => ({
834
+ ...k,
835
+ value: mergeContours(sources.map((s) => s.kind === "static" ? s.value : s.keys[j].value))
836
+ }))
837
+ };
838
+ ctx.warnings.push(`[approximation] ${where}: merge-paths members with misaligned keys baked densely at ${ctx.doc.fr} fps`);
839
+ const trackOf = (s) => track("__bake/d", "path", s.keys);
840
+ const samplers = sources.map((s) => s.kind === "static" ? () => s.value : ((tr) => (t) => sampleTrack(tr, t))(trackOf(s)));
841
+ const times = animated.flatMap((s) => s.keys.map((k) => k.t));
842
+ const t0 = Math.min(...times);
843
+ const t1 = Math.max(...times);
844
+ const step = 1 / ctx.doc.fr;
845
+ const keys = [];
846
+ for (let t = t0; t < t1 + step / 2; t += step) {
847
+ const at = Math.min(t, t1);
848
+ keys.push({
849
+ t: at,
850
+ value: mergeContours(samplers.map((s) => s(at)))
851
+ });
852
+ }
853
+ return {
854
+ kind: "animated",
855
+ keys: enforceMonotonic(keys)
856
+ };
857
+ }
858
+ function pathSpecFor(ctx, style, geom, idBase, tm) {
859
+ const spec = {
860
+ kind: "path",
861
+ id: uid(ctx, idBase),
862
+ data: geom.kind === "static" ? geom.value : geom.keys[0].value
863
+ };
864
+ if (geom.kind === "animated") pushTrack(ctx, `${spec.id}/d`, "path", geom.keys.map((k) => ({ ...k })));
865
+ const colorTarget = style.ty === "fl" ? "fill" : "stroke";
866
+ const cNorm = norms(style.c);
867
+ if (cNorm) {
868
+ const bytes = colorPropIsBytes(cNorm.map((k) => k.value));
869
+ const keys = convertKeys(cNorm, tm, (v) => lottieColor(v, bytes));
870
+ spec[colorTarget] = keys[0].value;
871
+ pushTrack(ctx, `${spec.id}/${colorTarget}`, "color", keys);
872
+ } else if (style.c?.k !== void 0) spec[colorTarget] = lottieColor(style.c.k, colorPropIsBytes([style.c.k]));
873
+ applyOpacity(ctx, spec, style.o, tm);
874
+ if (style.ty === "st") {
875
+ applyScalarProp(ctx, spec, style.w, "strokeWidth", (v) => v, (v) => spec.strokeWidth = v, tm);
876
+ spec.strokeWidth ??= 1;
877
+ }
878
+ return spec;
879
+ }
880
+ /**
881
+ * One items list → children, bottom-to-top (Lottie paints array-first on TOP,
882
+ * glissade Groups paint array-last on top, so emission walks in reverse).
883
+ * Each fill/stroke becomes one Path node per preceding geometry; an mm mode-1
884
+ * merges the list's geometries into a single multi-contour source first.
885
+ */
886
+ function denormItems(ctx, items, idBase, tm, where) {
887
+ const visible = items.filter((it) => it.hd !== true);
888
+ const hasMerge = visible.some((it) => it.ty === "mm" && (it.mm ?? 1) === 1);
889
+ const slots = [];
890
+ const geoms = [];
891
+ let geomCounter = 0;
892
+ for (let i = 0; i < visible.length; i++) {
893
+ const item = visible[i];
894
+ const here = `${where}/${item.nm ?? item.ty}`;
895
+ if (item.ty === "gr") {
896
+ const inner = item.it?.filter((it) => it.hd !== true) ?? [];
897
+ const tr = inner.find((it) => it.ty === "tr");
898
+ const grSpec = {
899
+ kind: "group",
900
+ id: uid(ctx, `${idBase}_${item.nm ?? `g${i}`}`),
901
+ children: []
902
+ };
903
+ let attach = grSpec;
904
+ if (tr) {
905
+ attach = applyPRS(ctx, grSpec, tr, tm);
906
+ applyOpacity(ctx, grSpec, tr.o, tm);
907
+ const oStatic = typeof tr.o?.k === "number" ? tr.o.k : void 0;
908
+ if (tr.o !== void 0 && oStatic !== 100) ctx.warnings.push(`[approximation] ${here}: shape-group opacity composites the subtree as a unit; Lottie multiplies per shape — overlapping translucent siblings may differ`);
909
+ }
910
+ attach.children.push(...denormItems(ctx, inner.filter((it) => it.ty !== "tr"), grSpec.id, tm, here));
911
+ slots.push({
912
+ index: i,
913
+ node: grSpec
914
+ });
915
+ } else if (item.ty === "sh" || item.ty === "el" || item.ty === "rc") {
916
+ const source = geometrySource(ctx, item, tm, here);
917
+ if (source) geoms.push({
918
+ index: i,
919
+ source,
920
+ name: item.nm ?? `geo${geomCounter++}`
921
+ });
922
+ } else if (item.ty === "fl" || item.ty === "st") {
923
+ const preceding = geoms.filter((g) => g.index < i);
924
+ if (preceding.length === 0) continue;
925
+ const styleNodes = (hasMerge ? [{
926
+ source: mergeGeomSources(ctx, preceding.map((g) => g.source), tm, here),
927
+ name: "merged"
928
+ }] : preceding).map((g) => pathSpecFor(ctx, item, g.source, `${idBase}_${item.nm ?? item.ty}_${g.name}`, tm));
929
+ slots.push({
930
+ index: i,
931
+ styleNodes
932
+ });
933
+ }
934
+ }
935
+ const out = [];
936
+ for (let s = slots.length - 1; s >= 0; s--) {
937
+ const slot = slots[s];
938
+ if (slot.node) out.push(slot.node);
939
+ if (slot.styleNodes) out.push(...slot.styleNodes);
940
+ }
941
+ return out;
942
+ }
943
+ function visibilityWrapper(ctx, base, layer, content, ind) {
944
+ const { doc } = ctx;
945
+ const docDur = (doc.op - doc.ip) / doc.fr;
946
+ const ipS = layer.ip / doc.fr + ctx.offset;
947
+ const opS = layer.op / doc.fr + ctx.offset;
948
+ if (ipS <= 0 && opS >= docDur) return content;
949
+ const vis = {
950
+ kind: "group",
951
+ id: uid(ctx, `${base}__v`),
952
+ children: [content],
953
+ zIndex: -ind
954
+ };
955
+ if (opS <= 0) {
956
+ vis.opacity = 0;
957
+ return vis;
958
+ }
959
+ const keys = [];
960
+ if (ipS > 0) {
961
+ keys.push({
962
+ t: 0,
963
+ value: 0
964
+ }, {
965
+ t: ipS,
966
+ value: 1,
967
+ interp: "hold"
968
+ });
969
+ vis.opacity = 0;
970
+ } else keys.push({
971
+ t: 0,
972
+ value: 1
973
+ });
974
+ if (opS < docDur) keys.push({
975
+ t: opS,
976
+ value: 0,
977
+ interp: "hold"
978
+ });
979
+ pushTrack(ctx, `${vis.id}/opacity`, "number", enforceMonotonic(keys));
980
+ return vis;
981
+ }
982
+ function convertLayer(ctx, layer, index) {
983
+ const ind = layer.ind ?? index;
984
+ const base = uid(ctx, layer.nm ?? `layer${ind}`);
985
+ const tm = {
986
+ fr: ctx.doc.fr,
987
+ st: layer.st ?? 0,
988
+ offset: ctx.offset
989
+ };
990
+ const outer = {
991
+ kind: "group",
992
+ id: base,
993
+ children: [],
994
+ zIndex: -ind
995
+ };
996
+ const attach = layer.ks ? applyPRS(ctx, outer, layer.ks, tm) : outer;
997
+ const contentChildren = [];
998
+ if (layer.ty === 4 && layer.shapes) contentChildren.push(...denormItems(ctx, layer.shapes, base, tm, `layer '${layer.nm ?? ind}'`));
999
+ else if (layer.ty === 1) {
1000
+ const w = layer.sw ?? 0;
1001
+ const h = layer.sh ?? 0;
1002
+ const solid = {
1003
+ kind: "rect",
1004
+ id: uid(ctx, `${base}__solid`),
1005
+ width: w,
1006
+ height: h,
1007
+ position: [w / 2, h / 2]
1008
+ };
1009
+ if (layer.sc !== void 0) solid.fill = layer.sc;
1010
+ contentChildren.push(solid);
1011
+ } else if (layer.ty === 2) {
1012
+ const asset = (ctx.doc.assets ?? []).find((a) => a.id === layer.refId);
1013
+ ctx.assets[asset.id] = {
1014
+ kind: "image",
1015
+ url: `${asset.u ?? ""}${asset.p ?? ""}`
1016
+ };
1017
+ const w = asset.w ?? 0;
1018
+ const h = asset.h ?? 0;
1019
+ const image = {
1020
+ kind: "image",
1021
+ id: uid(ctx, `${base}__img`),
1022
+ assetId: asset.id,
1023
+ width: w,
1024
+ height: h,
1025
+ position: [w / 2, h / 2]
1026
+ };
1027
+ contentChildren.push(image);
1028
+ }
1029
+ if (layer.ty !== 3 && layer.hd !== true) {
1030
+ const content = {
1031
+ kind: "group",
1032
+ id: uid(ctx, `${base}__c`),
1033
+ children: contentChildren,
1034
+ zIndex: -ind
1035
+ };
1036
+ applyOpacity(ctx, content, layer.ks?.o, tm);
1037
+ attach.children.push(visibilityWrapper(ctx, base, layer, content, ind));
1038
+ }
1039
+ return {
1040
+ layer,
1041
+ outer,
1042
+ attach
1043
+ };
1044
+ }
1045
+ function convertDocument(doc, warnings) {
1046
+ const ctx = {
1047
+ doc,
1048
+ tracks: [],
1049
+ warnings,
1050
+ assets: {},
1051
+ ids: /* @__PURE__ */ new Set(),
1052
+ offset: -doc.ip / doc.fr
1053
+ };
1054
+ const records = doc.layers.map((layer, i) => convertLayer(ctx, layer, i));
1055
+ const byInd = /* @__PURE__ */ new Map();
1056
+ records.forEach((r, i) => byInd.set(r.layer.ind ?? i, r));
1057
+ const roots = [];
1058
+ for (const r of records) {
1059
+ const parent = r.layer.parent !== void 0 ? byInd.get(r.layer.parent) : void 0;
1060
+ if (parent && parent !== r) parent.attach.children.push(r.outer);
1061
+ else roots.push(r.outer);
1062
+ }
1063
+ const timeline = {
1064
+ version: 1,
1065
+ duration: (doc.op - doc.ip) / doc.fr,
1066
+ fps: doc.fr,
1067
+ tracks: ctx.tracks,
1068
+ ...Object.keys(ctx.assets).length > 0 ? { assets: ctx.assets } : {}
1069
+ };
1070
+ return {
1071
+ size: {
1072
+ w: doc.w,
1073
+ h: doc.h
1074
+ },
1075
+ nodes: roots,
1076
+ timeline,
1077
+ warnings: ctx.warnings
1078
+ };
1079
+ }
1080
+ //#endregion
1081
+ //#region src/codegen.ts
1082
+ const CTOR = {
1083
+ group: "Group",
1084
+ path: "Path",
1085
+ rect: "Rect",
1086
+ image: "ImageNode"
1087
+ };
1088
+ function lit(value, indent) {
1089
+ return JSON.stringify(value, null, 2).split("\n").join(`\n${indent}`);
1090
+ }
1091
+ function emitNode(spec, indent) {
1092
+ const inner = `${indent} `;
1093
+ const props = [`id: ${JSON.stringify(spec.id)}`];
1094
+ if (spec.position) props.push(`position: ${JSON.stringify(spec.position)}`);
1095
+ if (spec.rotation !== void 0) props.push(`rotation: ${spec.rotation}`);
1096
+ if (spec.scale) props.push(`scale: ${JSON.stringify(spec.scale)}`);
1097
+ if (spec.opacity !== void 0) props.push(`opacity: ${spec.opacity}`);
1098
+ if (spec.zIndex !== void 0) props.push(`zIndex: ${spec.zIndex}`);
1099
+ switch (spec.kind) {
1100
+ case "group":
1101
+ if (spec.children.length > 0) {
1102
+ const children = spec.children.map((c) => `${inner} ${emitNode(c, `${inner} `)},`).join("\n");
1103
+ props.push(`children: [\n${children}\n${inner}]`);
1104
+ }
1105
+ break;
1106
+ case "path":
1107
+ props.push(`data: ${lit(spec.data, inner)} as PathValue`);
1108
+ if (spec.fill !== void 0) props.push(`fill: ${JSON.stringify(spec.fill)}`);
1109
+ if (spec.stroke !== void 0) props.push(`stroke: ${JSON.stringify(spec.stroke)}`);
1110
+ if (spec.strokeWidth !== void 0) props.push(`strokeWidth: ${spec.strokeWidth}`);
1111
+ break;
1112
+ case "rect":
1113
+ props.push(`width: ${spec.width}`, `height: ${spec.height}`);
1114
+ if (spec.fill !== void 0) props.push(`fill: ${JSON.stringify(spec.fill)}`);
1115
+ break;
1116
+ case "image":
1117
+ props.push(`assetId: ${JSON.stringify(spec.assetId)}`, `width: ${spec.width}`, `height: ${spec.height}`);
1118
+ break;
1119
+ }
1120
+ return `new ${CTOR[spec.kind]}({\n${props.map((p) => `${inner}${p},`).join("\n")}\n${indent}})`;
1121
+ }
1122
+ function generateSceneModule(result, opts = {}) {
1123
+ const usedKinds = /* @__PURE__ */ new Set();
1124
+ const visit = (s) => {
1125
+ usedKinds.add(s.kind);
1126
+ if (s.kind === "group") s.children.forEach(visit);
1127
+ };
1128
+ result.nodes.forEach(visit);
1129
+ const ctors = [...usedKinds].map((k) => CTOR[k]).sort();
1130
+ const usesPath = usedKinds.has("path");
1131
+ const children = result.nodes.map((n) => ` ${emitNode(n, " ")},`).join("\n");
1132
+ return `${`// Generated by gs import${opts.source ? ` from ${opts.source}` : ""} (@glissade/lottie).`}${result.warnings.length ? `\n// Import warnings:\n${result.warnings.map((w) => `// ${w}`).join("\n")}` : ""}
1133
+ import { ${ctors.join(", ")}, createScene, type SceneModule } from '@glissade/scene';
1134
+ import type { ${usesPath ? "PathValue, " : ""}Timeline } from '@glissade/core';
1135
+
1136
+ export const timeline = ${lit(result.timeline, "")} as unknown as Timeline;
1137
+
1138
+ export default {
1139
+ createScene: () =>
1140
+ createScene({
1141
+ size: ${JSON.stringify(result.size)},
1142
+ children: [
1143
+ ${children}
1144
+ ],
1145
+ }),
1146
+ timeline,
1147
+ } satisfies SceneModule;
1148
+ `;
1149
+ }
1150
+ //#endregion
1151
+ //#region src/index.ts
1152
+ function assertDocument(json) {
1153
+ const doc = json;
1154
+ if (doc === null || typeof doc !== "object" || typeof doc.fr !== "number" || doc.fr <= 0 || typeof doc.ip !== "number" || typeof doc.op !== "number" || typeof doc.w !== "number" || typeof doc.h !== "number" || !Array.isArray(doc.layers)) throw new LottieImportError(["[invalid-document] not a Lottie document (missing fr/ip/op/w/h/layers)"]);
1155
+ return doc;
1156
+ }
1157
+ function importLottie(json, opts = {}) {
1158
+ const doc = assertDocument(json);
1159
+ const { warnings } = auditDocument(doc, opts.allowDegraded === true);
1160
+ const out = convertDocument(doc, warnings);
1161
+ return {
1162
+ ...out,
1163
+ toSceneModule() {
1164
+ return {
1165
+ createScene: () => createScene({
1166
+ size: out.size,
1167
+ children: buildNodes(out.nodes)
1168
+ }),
1169
+ timeline: out.timeline
1170
+ };
1171
+ }
1172
+ };
1173
+ }
1174
+ //#endregion
1175
+ export { KAPPA, LottieImportError, buildNode, buildNodes, colorPropIsBytes, ellipseContour, generateSceneModule, importLottie, lottieColor, mergeContours, rectContour, reverseContour, shToContour };