@glissade/lottie 0.44.0 → 0.45.0-pre.1

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.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as _glissade_scene0 from "@glissade/scene";
2
- import { Node } from "@glissade/scene";
2
+ import { Node, SceneModule } from "@glissade/scene";
3
3
  import { PathContour, PathValue, Timeline, Vec2 } from "@glissade/core";
4
4
 
5
5
  //#region src/spec.d.ts
@@ -64,12 +64,124 @@ interface CodegenOptions {
64
64
  declare function generateSceneModule(result: LottieImportResult, opts?: CodegenOptions): string;
65
65
  //#endregion
66
66
  //#region src/types.d.ts
67
+ /**
68
+ * Loose typings for the subset of the Lottie/bodymovin JSON schema the S1
69
+ * importer reads. Old bodymovin exports (s/e keyframe pairs, top-level
70
+ * `closed` on sh, 0–255 colors) and modern exports (s-only keys, `c` inside
71
+ * the path object, 0–1 colors) are both accepted.
72
+ */
73
+ /** Animatable property: static (`k` value) or keyframed (`k` array of LottieKeyframe). */
74
+ interface LottieProp {
75
+ a?: number;
76
+ k?: unknown;
77
+ /** Expression source (audited; stripped under allowDegraded). */
78
+ x?: unknown;
79
+ ix?: number | string;
80
+ }
81
+ /** Split position: `{ s: true, x, y }` — per-axis animatable scalars. */
82
+ interface LottieSplitPosition {
83
+ s: true;
84
+ x: LottieProp;
85
+ y: LottieProp;
86
+ }
67
87
  interface LottieShapePathData {
68
88
  v: number[][];
69
89
  i: number[][];
70
90
  o: number[][];
71
91
  c?: boolean;
72
92
  }
93
+ interface LottieShapeItem {
94
+ /** shape direction: 3 = reversed winding (el/rc). */
95
+ d?: number | {
96
+ k?: unknown;
97
+ };
98
+ ty: string;
99
+ nm?: string;
100
+ hd?: boolean;
101
+ /** gr */
102
+ it?: LottieShapeItem[];
103
+ /** sh */
104
+ ks?: LottieProp;
105
+ closed?: boolean;
106
+ /** el / rc / tr */
107
+ p?: LottieProp;
108
+ s?: LottieProp;
109
+ a?: LottieProp;
110
+ /** rc corner radius / fl-st opacity-adjacent fields */
111
+ r?: LottieProp | number;
112
+ /** fl / st */
113
+ c?: LottieProp;
114
+ o?: LottieProp;
115
+ w?: LottieProp;
116
+ /** mm */
117
+ mm?: number;
118
+ }
119
+ interface LottieTransform {
120
+ a?: LottieProp;
121
+ p?: LottieProp | LottieSplitPosition;
122
+ s?: LottieProp;
123
+ r?: LottieProp;
124
+ o?: LottieProp;
125
+ sk?: LottieProp;
126
+ sa?: LottieProp;
127
+ }
128
+ interface LottieLayer {
129
+ ty: number;
130
+ nm?: string;
131
+ /** Hidden: never rendered — skipped by audit and conversion alike. */
132
+ hd?: boolean;
133
+ ind?: number;
134
+ parent?: number;
135
+ ip: number;
136
+ op: number;
137
+ st?: number;
138
+ sr?: number;
139
+ ks?: LottieTransform;
140
+ ddd?: number;
141
+ ao?: number;
142
+ /** shape layer */
143
+ shapes?: LottieShapeItem[];
144
+ /** solid */
145
+ sw?: number;
146
+ sh?: number;
147
+ sc?: string;
148
+ /** image / precomp */
149
+ refId?: string;
150
+ /** rejections */
151
+ masksProperties?: unknown[];
152
+ hasMask?: boolean;
153
+ tt?: number;
154
+ td?: number;
155
+ tm?: unknown;
156
+ ef?: unknown[];
157
+ sy?: unknown[];
158
+ w?: number;
159
+ h?: number;
160
+ }
161
+ interface LottieAsset {
162
+ id: string;
163
+ /** image assets */
164
+ w?: number;
165
+ h?: number;
166
+ p?: string;
167
+ u?: string;
168
+ e?: number;
169
+ /** precomp assets */
170
+ layers?: LottieLayer[];
171
+ }
172
+ interface LottieDocument {
173
+ /** bodymovin schema version (`v`) — strict lottie-web/dotLottie validators require it. */
174
+ v?: string;
175
+ fr: number;
176
+ ip: number;
177
+ op: number;
178
+ w: number;
179
+ h: number;
180
+ nm?: string;
181
+ ddd?: number;
182
+ layers: LottieLayer[];
183
+ assets?: LottieAsset[];
184
+ }
73
185
  //#endregion
74
186
  //#region src/pathvalue.d.ts
75
187
  /** sh data → one contour. `closedFallback` covers the old top-level `closed` flag. */
@@ -103,6 +215,18 @@ declare function lottieColor(value: unknown, bytes: boolean): string;
103
215
  /** True when ANY component across the prop's values exceeds 1 (old byte exports). */
104
216
  declare function colorPropIsBytes(values: unknown[]): boolean;
105
217
  //#endregion
218
+ //#region src/export.d.ts
219
+ interface ExportOptions {
220
+ width: number;
221
+ height: number;
222
+ /** Frame rate; default the timeline's fps, else 60 (the golden FPS). */
223
+ fps?: number;
224
+ /** Sink for scope-out / degrade warnings; default `console.warn`. */
225
+ onWarn?: (message: string) => void;
226
+ }
227
+ /** Convert a SceneModule to a Lottie document. Pure over (scene, timeline). */
228
+ declare function exportLottie(mod: SceneModule, opts: ExportOptions): LottieDocument;
229
+ //#endregion
106
230
  //#region src/index.d.ts
107
231
  interface ImportOptions {
108
232
  /** Downgrade degradable rejections (expressions, merge-paths modes ≠ 1) to warnings. */
@@ -110,4 +234,4 @@ interface ImportOptions {
110
234
  }
111
235
  declare function importLottie(json: unknown, opts?: ImportOptions): LottieImportResult;
112
236
  //#endregion
113
- export { type CodegenOptions, type GroupSpec, type ImageSpec, ImportOptions, KAPPA, LottieImportError, type LottieImportResult, type NodeSpec, type PathSpec, type RectSpec, buildNode, buildNodes, colorPropIsBytes, ellipseContour, generateSceneModule, importLottie, lottieColor, mergeContours, rectContour, reverseContour, shToContour };
237
+ export { type CodegenOptions, type ExportOptions, type GroupSpec, type ImageSpec, ImportOptions, KAPPA, type LottieDocument, LottieImportError, type LottieImportResult, type NodeSpec, type PathSpec, type RectSpec, buildNode, buildNodes, colorPropIsBytes, ellipseContour, exportLottie, generateSceneModule, importLottie, lottieColor, mergeContours, rectContour, reverseContour, shToContour };
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
- import { Group, ImageNode, Path, Rect, createScene } from "@glissade/scene";
2
- import { cubicBezier, formatColor, sampleTrack, track } from "@glissade/core";
1
+ import { Circle, Group, ImageNode, Path, Rect, createScene } from "@glissade/scene";
2
+ import { cubicBezier, formatColor, parseColor, sampleTrack, track } from "@glissade/core";
3
+ import "@glissade/core/expr";
3
4
  //#region src/spec.ts
4
5
  var LottieImportError = class extends Error {
5
6
  problems;
@@ -1148,6 +1149,631 @@ ${children}
1148
1149
  `;
1149
1150
  }
1150
1151
  //#endregion
1152
+ //#region src/emitGeometry.ts
1153
+ const pt = (p) => [p[0], p[1]];
1154
+ /** One PathContour → one Lottie sh path object (inverse of shToContour). */
1155
+ function contourToShData(c) {
1156
+ return {
1157
+ v: c.v.map(pt),
1158
+ i: c.in.map(pt),
1159
+ o: c.out.map(pt),
1160
+ c: c.closed
1161
+ };
1162
+ }
1163
+ /**
1164
+ * A PathValue (contour list) → the Lottie sh keyframe `s` payload: an array of
1165
+ * path objects (the importer's `toValue` maps `Array.isArray(v) ? v : [v]`, so
1166
+ * an array is the general, always-correct shape — including the single-contour
1167
+ * case).
1168
+ */
1169
+ function pathValueToShData(pv) {
1170
+ return pv.map(contourToShData);
1171
+ }
1172
+ //#endregion
1173
+ //#region src/emitKeyframes.ts
1174
+ /** Seconds → Lottie frame index (offset 0, st 0), matching the importer's toSeconds inverse. */
1175
+ const toFrames = (tSec, fr) => Math.round(tSec * fr);
1176
+ /**
1177
+ * A cubicBezier / linear ease → the DEPARTING key's `o`/`i` handles. Linear
1178
+ * (undefined ease) is the bezier identity `o:{0,0} i:{1,1}` — which the importer
1179
+ * reads back as linear (departingEase returns undefined when x≈y on both
1180
+ * handles). Returns undefined for an ease that is NOT a plain bezier (a named
1181
+ * string or a spring) — the caller must sample that track instead.
1182
+ */
1183
+ function easeHandles(ease) {
1184
+ if (ease === void 0) return {
1185
+ o: {
1186
+ x: 0,
1187
+ y: 0
1188
+ },
1189
+ i: {
1190
+ x: 1,
1191
+ y: 1
1192
+ }
1193
+ };
1194
+ if (typeof ease === "object" && ease.kind === "cubicBezier") {
1195
+ const [x1, y1, x2, y2] = ease.pts;
1196
+ return {
1197
+ o: {
1198
+ x: x1,
1199
+ y: y1
1200
+ },
1201
+ i: {
1202
+ x: x2,
1203
+ y: y2
1204
+ }
1205
+ };
1206
+ }
1207
+ }
1208
+ /**
1209
+ * True when EVERY segment of the track is directly invertible to a Lottie
1210
+ * bezier: linear (no ease), cubicBezier, or hold. A named-string ease, a spring,
1211
+ * or an expr track is not — those are baked by sampling. (The ease lives on the
1212
+ * ARRIVING key, so segment j is described by `keys[j]`.)
1213
+ */
1214
+ function isDirectlyInvertible(keys, expr) {
1215
+ if (expr !== void 0) return false;
1216
+ for (let j = 1; j < keys.length; j++) {
1217
+ const k = keys[j];
1218
+ if (k.interp === "hold") continue;
1219
+ if (k.ease === void 0) continue;
1220
+ if (typeof k.ease === "object" && k.ease.kind === "cubicBezier") continue;
1221
+ return false;
1222
+ }
1223
+ return true;
1224
+ }
1225
+ /**
1226
+ * Emit Lottie keyframes with the ease shift: the ease/hold glissade stores on
1227
+ * ARRIVING key j+1 becomes the DEPARTING handles/hold of Lottie key j. `toS`
1228
+ * maps a glissade value to the Lottie `s` payload (a scalar `[v]`, a vec2
1229
+ * `[x,y]`, or an sh path-data array). Assumes {@link isDirectlyInvertible}.
1230
+ */
1231
+ function emitKeys(keys, fr, toS) {
1232
+ const out = [];
1233
+ for (let j = 0; j < keys.length; j++) {
1234
+ const k = keys[j];
1235
+ const frame = {
1236
+ t: toFrames(k.t, fr),
1237
+ s: toS(k.value)
1238
+ };
1239
+ const next = keys[j + 1];
1240
+ if (next !== void 0) if (next.interp === "hold") frame.h = 1;
1241
+ else {
1242
+ const h = easeHandles(next.ease);
1243
+ if (h !== void 0) {
1244
+ frame.o = h.o;
1245
+ frame.i = h.i;
1246
+ }
1247
+ }
1248
+ out.push(frame);
1249
+ }
1250
+ return out;
1251
+ }
1252
+ //#endregion
1253
+ //#region src/sampleFallback.ts
1254
+ /**
1255
+ * Sample `tr` at every integer frame across its keyed span (or the whole
1256
+ * document `[ip, op]` for an expr track with no keys) and emit linear Lottie
1257
+ * keys, then DECIMATE redundant ones ({@link decimateLinearKeys}). `toS` maps a
1258
+ * sampled value to the Lottie `s` payload.
1259
+ *
1260
+ * Dense per-frame sampling is faithful but huge — a spring or named ease over a
1261
+ * long shot densifies to one key per frame on every channel (a real episode
1262
+ * measured ~148k keys / 139 MB). Since Lottie plays LINEAR between keys, most of
1263
+ * those samples lie on the straight run between two others and are redundant;
1264
+ * decimation drops them within a per-component tolerance, collapsing constant and
1265
+ * constant-velocity stretches to their endpoints while keeping the curvature.
1266
+ */
1267
+ function sampleToLottieKeys(tr, fr, ip, op, toS) {
1268
+ const keys = tr.keys;
1269
+ let f0 = ip;
1270
+ let f1 = op;
1271
+ if (keys.length > 0) {
1272
+ f0 = toFrames(keys[0].t, fr);
1273
+ f1 = toFrames(keys[keys.length - 1].t, fr);
1274
+ }
1275
+ const out = [];
1276
+ for (let f = f0; f <= f1; f++) {
1277
+ const value = sampleTrack(tr, f / fr);
1278
+ const frame = {
1279
+ t: f,
1280
+ s: toS(value)
1281
+ };
1282
+ if (f < f1) {
1283
+ frame.o = {
1284
+ x: 0,
1285
+ y: 0
1286
+ };
1287
+ frame.i = {
1288
+ x: 1,
1289
+ y: 1
1290
+ };
1291
+ }
1292
+ out.push(frame);
1293
+ }
1294
+ return decimateLinearKeys(out);
1295
+ }
1296
+ /**
1297
+ * Ramer–Douglas–Peucker over linear-interpolated keyframes: keep the endpoints
1298
+ * plus any interior key whose value is NOT reproduced (within `relEps` of each
1299
+ * component's range) by linear interpolation between the kept neighbors — those
1300
+ * are exactly the keys Lottie's linear playback can't recreate, so the rest are
1301
+ * safe to drop. Endpoints (first/last, hence the exact start/end value and time)
1302
+ * are always kept; existing linear handles on the survivors stay valid. Only
1303
+ * flat-numeric `s` payloads are decimated — path `sh` data (nested vertex arrays)
1304
+ * is left dense. `relEps` is a fraction of each channel's value range, so it is
1305
+ * scale-invariant across position (px), opacity (0–100), scale (×100), color (0–1).
1306
+ */
1307
+ function decimateLinearKeys(keys, relEps = .002) {
1308
+ const n = keys.length;
1309
+ if (n <= 2) return keys;
1310
+ const isFlat = (s) => Array.isArray(s) && s.every((x) => typeof x === "number");
1311
+ if (!keys.every((k) => isFlat(k.s))) return keys;
1312
+ const sAt = (i) => keys[i].s;
1313
+ const dim = sAt(0).length;
1314
+ const min = new Array(dim).fill(Infinity);
1315
+ const max = new Array(dim).fill(-Infinity);
1316
+ for (let i = 0; i < n; i++) {
1317
+ const s = sAt(i);
1318
+ for (let c = 0; c < dim; c++) {
1319
+ const v = s[c];
1320
+ if (v < min[c]) min[c] = v;
1321
+ if (v > max[c]) max[c] = v;
1322
+ }
1323
+ }
1324
+ const invRange = new Array(dim);
1325
+ for (let c = 0; c < dim; c++) {
1326
+ const r = max[c] - min[c];
1327
+ invRange[c] = r > 1e-9 ? 1 / r : 0;
1328
+ }
1329
+ const keep = new Uint8Array(n);
1330
+ keep[0] = 1;
1331
+ keep[n - 1] = 1;
1332
+ const stack = [[0, n - 1]];
1333
+ while (stack.length > 0) {
1334
+ const [lo, hi] = stack.pop();
1335
+ if (hi - lo < 2) continue;
1336
+ const a = sAt(lo);
1337
+ const b = sAt(hi);
1338
+ const ta = keys[lo].t;
1339
+ const span = keys[hi].t - ta;
1340
+ let worst = -1;
1341
+ let worstDev = relEps;
1342
+ for (let i = lo + 1; i < hi; i++) {
1343
+ const s = sAt(i);
1344
+ const f = span > 0 ? (keys[i].t - ta) / span : 0;
1345
+ let dev = 0;
1346
+ for (let c = 0; c < dim; c++) {
1347
+ const chord = a[c] + (b[c] - a[c]) * f;
1348
+ const d = Math.abs(s[c] - chord) * invRange[c];
1349
+ if (d > dev) dev = d;
1350
+ }
1351
+ if (dev > worstDev) {
1352
+ worstDev = dev;
1353
+ worst = i;
1354
+ }
1355
+ }
1356
+ if (worst >= 0) {
1357
+ keep[worst] = 1;
1358
+ stack.push([lo, worst], [worst, hi]);
1359
+ }
1360
+ }
1361
+ const out = [];
1362
+ for (let i = 0; i < n; i++) if (keep[i] === 1) out.push(keys[i]);
1363
+ return out;
1364
+ }
1365
+ //#endregion
1366
+ //#region src/export.ts
1367
+ /**
1368
+ * @glissade/lottie EXPORT (Track → Lottie): a SceneModule → a `LottieDocument`,
1369
+ * the shipped importer (`convert.ts`) read backwards. Every mapping here is the
1370
+ * exact inverse of an import step, so a mappable scene round-trips through
1371
+ * `importLottie` faithfully (see test/roundtrip.test.ts).
1372
+ *
1373
+ * INPUT is the SceneModule (`{ createScene, timeline }`), NOT a flattened
1374
+ * DisplayList: Lottie is an ANIMATED, HIERARCHICAL format, so the node TREE +
1375
+ * the Timeline are the correct source. The walk emits one Lottie layer per node
1376
+ * (Group → a null/ty:3 transform parent; Rect/Circle/Path → a ty:4 shape layer),
1377
+ * parents children via `ind`/`parent`, and turns each `<id>/<prop>` track into an
1378
+ * animated Lottie channel (no track → a static `{a:0,k}` sampled at t=0).
1379
+ *
1380
+ * EASE FIDELITY: cubicBezier + hold + linear eases invert EXACTLY (emitKeyframes).
1381
+ * Named eases, springs, and expr formulas can't be one Lottie bezier — those are
1382
+ * baked to dense linear keys by sampling on the frame grid (sampleFallback), the
1383
+ * same discipline the importer uses for misaligned parametric geometry.
1384
+ *
1385
+ * SCOPE (MVP — mirror the importer's audit discipline: warn + drop, never
1386
+ * silent). IN: Group hierarchy, Rect/Circle/Path with a SOLID fill (+ optional
1387
+ * stroke), transform channels (position / position.x/.y split, opacity, scale,
1388
+ * rotation → identity degrees), animated `fill` color, animated `d` path
1389
+ * (constant topology). OUT (warned + dropped): Text, Image/Video, gradient/mesh
1390
+ * paint (solid only), non-center anchors, group opacity compositing (Lottie
1391
+ * parenting never inherits opacity). Animated primitive geometry (width/radius
1392
+ * tracks) is SAMPLED, not channel-mapped.
1393
+ */
1394
+ const EMPTY_TRACKS = /* @__PURE__ */ new Map();
1395
+ /** Convert a SceneModule to a Lottie document. Pure over (scene, timeline). */
1396
+ function exportLottie(mod, opts) {
1397
+ const scene = mod.createScene();
1398
+ const fr = opts.fps ?? mod.timeline.fps ?? 60;
1399
+ const warn = opts.onWarn ?? ((m) => console.warn(`gs export: ${m}`));
1400
+ const byNode = /* @__PURE__ */ new Map();
1401
+ for (const tr of mod.timeline.tracks) {
1402
+ const resolved = resolveTrackNode(scene.nodes, tr.target);
1403
+ if (resolved === void 0) {
1404
+ warn(`track '${tr.target}' targets no node in the scene — dropped`);
1405
+ continue;
1406
+ }
1407
+ const [nodeId, prop] = resolved;
1408
+ let m = byNode.get(nodeId);
1409
+ if (m === void 0) {
1410
+ m = /* @__PURE__ */ new Map();
1411
+ byNode.set(nodeId, m);
1412
+ }
1413
+ m.set(prop, tr);
1414
+ }
1415
+ const op = computeOp(mod.timeline, fr);
1416
+ const ctx = {
1417
+ fr,
1418
+ ip: 0,
1419
+ op,
1420
+ warn,
1421
+ layers: [],
1422
+ ind: 0
1423
+ };
1424
+ walkChildren(ctx, scene.root.children, void 0, byNode);
1425
+ return {
1426
+ v: BODYMOVIN_VERSION,
1427
+ fr,
1428
+ ip: 0,
1429
+ op,
1430
+ w: opts.width,
1431
+ h: opts.height,
1432
+ nm: "glissade export",
1433
+ layers: ctx.layers
1434
+ };
1435
+ }
1436
+ /**
1437
+ * bodymovin schema version stamped on every export. Strict lottie-web / dotLottie
1438
+ * validators reject a document without a top-level `v`; 5.7.x is a widely-supported
1439
+ * modern version and the shape this exporter emits is a subset of it.
1440
+ */
1441
+ const BODYMOVIN_VERSION = "5.7.0";
1442
+ /** Resolve a track target to `[nodeId, propPath]` by the longest registered-id prefix. */
1443
+ function resolveTrackNode(nodes, target) {
1444
+ for (let slash = target.lastIndexOf("/"); slash > 0; slash = target.lastIndexOf("/", slash - 1)) {
1445
+ const id = target.slice(0, slash);
1446
+ if (nodes.has(id)) return [id, target.slice(slash + 1)];
1447
+ }
1448
+ }
1449
+ /** Document out-point: the timeline duration, else the max key time (≥ 1 frame). */
1450
+ function computeOp(tl, fr) {
1451
+ if (tl.duration !== void 0) return Math.max(1, Math.round(tl.duration * fr));
1452
+ let maxT = 0;
1453
+ for (const tr of tl.tracks) {
1454
+ const last = tr.keys[tr.keys.length - 1];
1455
+ if (last) maxT = Math.max(maxT, last.t);
1456
+ }
1457
+ return Math.max(1, Math.round(maxT * fr));
1458
+ }
1459
+ /**
1460
+ * Emit a sibling list in REVERSE array order so the array-LAST (top-painted)
1461
+ * node gets the SMALLER `ind` — the importer reconstructs paint order from
1462
+ * `zIndex = -ind`, so a descending ind per sibling group preserves it.
1463
+ */
1464
+ function walkChildren(ctx, children, parentInd, byNode) {
1465
+ for (let i = children.length - 1; i >= 0; i--) {
1466
+ const node = children[i];
1467
+ const kind = classify(node);
1468
+ if (kind === "drop") {
1469
+ ctx.warn(`${describe(node)} is not exportable (MVP: Group / Rect / Circle / Path) — dropped`);
1470
+ continue;
1471
+ }
1472
+ const myInd = ++ctx.ind;
1473
+ const tracks = (node.id !== void 0 ? byNode.get(node.id) : void 0) ?? EMPTY_TRACKS;
1474
+ ctx.layers.push(kind === "group" ? buildNullLayer(ctx, node, myInd, parentInd, tracks) : buildShapeLayer(ctx, node, kind, myInd, parentInd, tracks));
1475
+ if (node instanceof Group) walkChildren(ctx, node.children, myInd, byNode);
1476
+ }
1477
+ }
1478
+ function classify(node) {
1479
+ if (node instanceof Rect) return "rect";
1480
+ if (node instanceof Circle) return "circle";
1481
+ if (node instanceof Path) return "path";
1482
+ if (node instanceof Group) return "group";
1483
+ return "drop";
1484
+ }
1485
+ const describe = (node) => `${node.describeType}${node.id !== void 0 ? ` '${node.id}'` : ""}`;
1486
+ function buildTransform(ctx, node, tracks) {
1487
+ if (node.hasAnchor && (node.anchor[0] !== .5 || node.anchor[1] !== .5)) ctx.warn(`${describe(node)}: a non-center anchor is not exported (MVP centers geometry) — placement may shift`);
1488
+ return {
1489
+ a: {
1490
+ a: 0,
1491
+ k: [0, 0]
1492
+ },
1493
+ p: positionProp(ctx, tracks, node.position()),
1494
+ s: vecProp(ctx, tracks, "scale", node.scale(), (v) => [v[0] * 100, v[1] * 100]),
1495
+ r: scalarProp(ctx, tracks, "rotation", node.rotation(), (v) => v),
1496
+ o: scalarProp(ctx, tracks, "opacity", node.opacity(), (v) => v * 100)
1497
+ };
1498
+ }
1499
+ function scalarProp(ctx, tracks, prop, staticVal, map) {
1500
+ const tr = tracks.get(prop);
1501
+ if (tr) return {
1502
+ a: 1,
1503
+ k: scalarKeys(ctx, tr, map)
1504
+ };
1505
+ return {
1506
+ a: 0,
1507
+ k: map(staticVal)
1508
+ };
1509
+ }
1510
+ function scalarKeys(ctx, tr, map) {
1511
+ const toS = (v) => [map(v)];
1512
+ return isDirectlyInvertible(tr.keys, tr.expr) ? emitKeys(tr.keys, ctx.fr, toS) : sampleToLottieKeys(tr, ctx.fr, ctx.ip, ctx.op, toS);
1513
+ }
1514
+ function vecProp(ctx, tracks, prop, staticVal, map) {
1515
+ const whole = tracks.get(prop);
1516
+ if (whole) return {
1517
+ a: 1,
1518
+ k: isDirectlyInvertible(whole.keys, whole.expr) ? emitKeys(whole.keys, ctx.fr, map) : sampleToLottieKeys(whole, ctx.fr, ctx.ip, ctx.op, map)
1519
+ };
1520
+ const xt = tracks.get(`${prop}.x`);
1521
+ const yt = tracks.get(`${prop}.y`);
1522
+ if (xt || yt) {
1523
+ ctx.warn(`per-axis '${prop}' animation is sampled at ${ctx.fr} fps into a combined channel`);
1524
+ return {
1525
+ a: 1,
1526
+ k: sampleComponentVec(ctx, xt, yt, staticVal, map)
1527
+ };
1528
+ }
1529
+ return {
1530
+ a: 0,
1531
+ k: map(staticVal)
1532
+ };
1533
+ }
1534
+ /** Position supports Lottie's native split form (`{s:true,x,y}`) for exactness. */
1535
+ function positionProp(ctx, tracks, staticPos) {
1536
+ const whole = tracks.get("position");
1537
+ if (whole) {
1538
+ const toS = (v) => [v[0], v[1]];
1539
+ return {
1540
+ a: 1,
1541
+ k: isDirectlyInvertible(whole.keys, whole.expr) ? emitKeys(whole.keys, ctx.fr, toS) : sampleToLottieKeys(whole, ctx.fr, ctx.ip, ctx.op, toS)
1542
+ };
1543
+ }
1544
+ const xt = tracks.get("position.x");
1545
+ const yt = tracks.get("position.y");
1546
+ if (xt || yt) return {
1547
+ s: true,
1548
+ x: xt ? {
1549
+ a: 1,
1550
+ k: scalarKeys(ctx, xt, (v) => v)
1551
+ } : {
1552
+ a: 0,
1553
+ k: staticPos[0]
1554
+ },
1555
+ y: yt ? {
1556
+ a: 1,
1557
+ k: scalarKeys(ctx, yt, (v) => v)
1558
+ } : {
1559
+ a: 0,
1560
+ k: staticPos[1]
1561
+ }
1562
+ };
1563
+ return {
1564
+ a: 0,
1565
+ k: [staticPos[0], staticPos[1]]
1566
+ };
1567
+ }
1568
+ function sampleComponentVec(ctx, xt, yt, staticVal, map) {
1569
+ const [f0, f1] = frameSpan(ctx, [xt, yt]);
1570
+ const out = [];
1571
+ for (let f = f0; f <= f1; f++) {
1572
+ const t = f / ctx.fr;
1573
+ const x = xt ? sampleTrack(xt, t) : staticVal[0];
1574
+ const y = yt ? sampleTrack(yt, t) : staticVal[1];
1575
+ const frame = {
1576
+ t: f,
1577
+ s: map([x, y])
1578
+ };
1579
+ if (f < f1) {
1580
+ frame.o = {
1581
+ x: 0,
1582
+ y: 0
1583
+ };
1584
+ frame.i = {
1585
+ x: 1,
1586
+ y: 1
1587
+ };
1588
+ }
1589
+ out.push(frame);
1590
+ }
1591
+ return out;
1592
+ }
1593
+ /** Union frame span of a set of tracks (their first→last key), else [ip, op]. */
1594
+ function frameSpan(ctx, tracks) {
1595
+ const bounds = [];
1596
+ for (const tr of tracks) if (tr && tr.keys.length > 0) bounds.push(toFrames(tr.keys[0].t, ctx.fr), toFrames(tr.keys[tr.keys.length - 1].t, ctx.fr));
1597
+ return bounds.length > 0 ? [Math.min(...bounds), Math.max(...bounds)] : [ctx.ip, ctx.op];
1598
+ }
1599
+ function buildNullLayer(ctx, node, ind, parentInd, tracks) {
1600
+ if (node.opacity() !== 1 || tracks.has("opacity")) ctx.warn(`${describe(node)}: group opacity is exported on the null layer, but Lottie parenting does not composite it over children`);
1601
+ return {
1602
+ ty: 3,
1603
+ nm: node.id ?? `group${ind}`,
1604
+ ind,
1605
+ ip: ctx.ip,
1606
+ op: ctx.op,
1607
+ ks: buildTransform(ctx, node, tracks),
1608
+ ...parentInd !== void 0 ? { parent: parentInd } : {}
1609
+ };
1610
+ }
1611
+ function buildShapeLayer(ctx, node, kind, ind, parentInd, tracks) {
1612
+ const shapes = [buildGeometry(ctx, node, kind, tracks)];
1613
+ const stroke = buildStroke(ctx, node, tracks);
1614
+ if (stroke) shapes.push(stroke);
1615
+ const fill = buildFill(ctx, node, tracks);
1616
+ if (fill) shapes.push(fill);
1617
+ return {
1618
+ ty: 4,
1619
+ nm: node.id ?? `shape${ind}`,
1620
+ ind,
1621
+ ip: ctx.ip,
1622
+ op: ctx.op,
1623
+ ks: buildTransform(ctx, node, tracks),
1624
+ shapes,
1625
+ ...parentInd !== void 0 ? { parent: parentInd } : {}
1626
+ };
1627
+ }
1628
+ function buildGeometry(ctx, node, kind, tracks) {
1629
+ if (kind === "path") return buildPathGeometry(ctx, node, tracks);
1630
+ const paramNames = kind === "rect" ? [
1631
+ "width",
1632
+ "height",
1633
+ "cornerRadius"
1634
+ ] : ["radius"];
1635
+ const contourAt = (t) => {
1636
+ if (kind === "rect") {
1637
+ const r = node;
1638
+ return rectContour([0, 0], [paramAt(tracks, "width", r.width(), t), paramAt(tracks, "height", r.height(), t)], paramAt(tracks, "cornerRadius", r.cornerRadius(), t));
1639
+ }
1640
+ const rad = paramAt(tracks, "radius", node.radius(), t);
1641
+ return ellipseContour([0, 0], [rad * 2, rad * 2]);
1642
+ };
1643
+ if (!paramNames.some((p) => tracks.has(p))) return {
1644
+ ty: "sh",
1645
+ ks: {
1646
+ a: 0,
1647
+ k: [contourToShData(contourAt(0))]
1648
+ }
1649
+ };
1650
+ ctx.warn(`${describe(node)}: animated primitive geometry is sampled at ${ctx.fr} fps (not channel-mapped)`);
1651
+ const [f0, f1] = frameSpan(ctx, paramNames.map((p) => tracks.get(p)));
1652
+ const keys = [];
1653
+ for (let f = f0; f <= f1; f++) {
1654
+ const frame = {
1655
+ t: f,
1656
+ s: [contourToShData(contourAt(f / ctx.fr))]
1657
+ };
1658
+ if (f < f1) {
1659
+ frame.o = {
1660
+ x: 0,
1661
+ y: 0
1662
+ };
1663
+ frame.i = {
1664
+ x: 1,
1665
+ y: 1
1666
+ };
1667
+ }
1668
+ keys.push(frame);
1669
+ }
1670
+ return {
1671
+ ty: "sh",
1672
+ ks: {
1673
+ a: 1,
1674
+ k: keys
1675
+ }
1676
+ };
1677
+ }
1678
+ function paramAt(tracks, prop, staticVal, t) {
1679
+ const tr = tracks.get(prop);
1680
+ return tr ? sampleTrack(tr, t) : staticVal;
1681
+ }
1682
+ function buildPathGeometry(ctx, node, tracks) {
1683
+ const tr = tracks.get("d");
1684
+ if (tr) return {
1685
+ ty: "sh",
1686
+ ks: {
1687
+ a: 1,
1688
+ k: isDirectlyInvertible(tr.keys, tr.expr) ? emitKeys(tr.keys, ctx.fr, pathValueToShData) : sampleToLottieKeys(tr, ctx.fr, ctx.ip, ctx.op, pathValueToShData)
1689
+ }
1690
+ };
1691
+ return {
1692
+ ty: "sh",
1693
+ ks: {
1694
+ a: 0,
1695
+ k: pathValueToShData(node.data())
1696
+ }
1697
+ };
1698
+ }
1699
+ function colorToLottie(css) {
1700
+ const { r, g, b, a } = parseColor(css);
1701
+ const base = [
1702
+ r / 255,
1703
+ g / 255,
1704
+ b / 255
1705
+ ];
1706
+ return a >= 1 ? base : [...base, a];
1707
+ }
1708
+ function colorKeys(ctx, tr) {
1709
+ return isDirectlyInvertible(tr.keys, tr.expr) ? emitKeys(tr.keys, ctx.fr, colorToLottie) : sampleToLottieKeys(tr, ctx.fr, ctx.ip, ctx.op, colorToLottie);
1710
+ }
1711
+ function buildFill(ctx, node, tracks) {
1712
+ const tr = tracks.get("fill");
1713
+ if (tr) {
1714
+ if (tr.type !== "color") {
1715
+ ctx.warn(`${describe(node)}: animated '${tr.type}' fill (gradient/mesh) is not exported — dropped`);
1716
+ return;
1717
+ }
1718
+ return {
1719
+ ty: "fl",
1720
+ c: {
1721
+ a: 1,
1722
+ k: colorKeys(ctx, tr)
1723
+ },
1724
+ o: {
1725
+ a: 0,
1726
+ k: 100
1727
+ }
1728
+ };
1729
+ }
1730
+ const fill = node.fill();
1731
+ if (typeof fill !== "string") {
1732
+ ctx.warn(`${describe(node)}: a gradient/mesh fill is not exported (MVP: solid color) — dropped`);
1733
+ return;
1734
+ }
1735
+ if (fill === "") return void 0;
1736
+ return {
1737
+ ty: "fl",
1738
+ c: {
1739
+ a: 0,
1740
+ k: colorToLottie(fill)
1741
+ },
1742
+ o: {
1743
+ a: 0,
1744
+ k: 100
1745
+ }
1746
+ };
1747
+ }
1748
+ function buildStroke(ctx, node, tracks) {
1749
+ const colorTr = tracks.get("stroke");
1750
+ const widthTr = tracks.get("strokeWidth");
1751
+ const staticStroke = node.stroke();
1752
+ const staticWidth = node.strokeWidth();
1753
+ if (!(colorTr !== void 0 || staticStroke !== "") || !(widthTr !== void 0 || staticWidth > 0)) return void 0;
1754
+ return {
1755
+ ty: "st",
1756
+ c: colorTr ? {
1757
+ a: 1,
1758
+ k: colorKeys(ctx, colorTr)
1759
+ } : {
1760
+ a: 0,
1761
+ k: colorToLottie(staticStroke)
1762
+ },
1763
+ o: {
1764
+ a: 0,
1765
+ k: 100
1766
+ },
1767
+ w: widthTr ? {
1768
+ a: 1,
1769
+ k: scalarKeys(ctx, widthTr, (v) => v)
1770
+ } : {
1771
+ a: 0,
1772
+ k: staticWidth
1773
+ }
1774
+ };
1775
+ }
1776
+ //#endregion
1151
1777
  //#region src/index.ts
1152
1778
  function assertDocument(json) {
1153
1779
  const doc = json;
@@ -1172,4 +1798,4 @@ function importLottie(json, opts = {}) {
1172
1798
  };
1173
1799
  }
1174
1800
  //#endregion
1175
- export { KAPPA, LottieImportError, buildNode, buildNodes, colorPropIsBytes, ellipseContour, generateSceneModule, importLottie, lottieColor, mergeContours, rectContour, reverseContour, shToContour };
1801
+ export { KAPPA, LottieImportError, buildNode, buildNodes, colorPropIsBytes, ellipseContour, exportLottie, generateSceneModule, importLottie, lottieColor, mergeContours, rectContour, reverseContour, shToContour };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glissade/lottie",
3
- "version": "0.44.0",
3
+ "version": "0.45.0-pre.1",
4
4
  "description": "glissade Lottie import (S1 MVP): pure .json (Lottie/bodymovin) → node specs + a v1 Timeline. Fail-fast feature audit; no DOM/Node dependencies.",
5
5
  "license": "Apache-2.0",
6
6
  "engines": {
@@ -18,8 +18,11 @@
18
18
  "dist"
19
19
  ],
20
20
  "dependencies": {
21
- "@glissade/core": "0.44.0",
22
- "@glissade/scene": "0.44.0"
21
+ "@glissade/core": "0.45.0-pre.1",
22
+ "@glissade/scene": "0.45.0-pre.1"
23
+ },
24
+ "devDependencies": {
25
+ "@glissade/backend-skia": "0.45.0-pre.1"
23
26
  },
24
27
  "repository": {
25
28
  "type": "git",