@glissade/lottie 0.44.0-pre.1 → 0.45.0-pre.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.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,122 @@ 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
+ fr: number;
174
+ ip: number;
175
+ op: number;
176
+ w: number;
177
+ h: number;
178
+ nm?: string;
179
+ ddd?: number;
180
+ layers: LottieLayer[];
181
+ assets?: LottieAsset[];
182
+ }
73
183
  //#endregion
74
184
  //#region src/pathvalue.d.ts
75
185
  /** sh data → one contour. `closedFallback` covers the old top-level `closed` flag. */
@@ -103,6 +213,18 @@ declare function lottieColor(value: unknown, bytes: boolean): string;
103
213
  /** True when ANY component across the prop's values exceeds 1 (old byte exports). */
104
214
  declare function colorPropIsBytes(values: unknown[]): boolean;
105
215
  //#endregion
216
+ //#region src/export.d.ts
217
+ interface ExportOptions {
218
+ width: number;
219
+ height: number;
220
+ /** Frame rate; default the timeline's fps, else 60 (the golden FPS). */
221
+ fps?: number;
222
+ /** Sink for scope-out / degrade warnings; default `console.warn`. */
223
+ onWarn?: (message: string) => void;
224
+ }
225
+ /** Convert a SceneModule to a Lottie document. Pure over (scene, timeline). */
226
+ declare function exportLottie(mod: SceneModule, opts: ExportOptions): LottieDocument;
227
+ //#endregion
106
228
  //#region src/index.d.ts
107
229
  interface ImportOptions {
108
230
  /** Downgrade degradable rejections (expressions, merge-paths modes ≠ 1) to warnings. */
@@ -110,4 +232,4 @@ interface ImportOptions {
110
232
  }
111
233
  declare function importLottie(json: unknown, opts?: ImportOptions): LottieImportResult;
112
234
  //#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 };
235
+ 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,547 @@ ${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. `toS` maps a sampled value to the Lottie `s` payload.
1258
+ */
1259
+ function sampleToLottieKeys(tr, fr, ip, op, toS) {
1260
+ const keys = tr.keys;
1261
+ let f0 = ip;
1262
+ let f1 = op;
1263
+ if (keys.length > 0) {
1264
+ f0 = toFrames(keys[0].t, fr);
1265
+ f1 = toFrames(keys[keys.length - 1].t, fr);
1266
+ }
1267
+ const out = [];
1268
+ for (let f = f0; f <= f1; f++) {
1269
+ const value = sampleTrack(tr, f / fr);
1270
+ const frame = {
1271
+ t: f,
1272
+ s: toS(value)
1273
+ };
1274
+ if (f < f1) {
1275
+ frame.o = {
1276
+ x: 0,
1277
+ y: 0
1278
+ };
1279
+ frame.i = {
1280
+ x: 1,
1281
+ y: 1
1282
+ };
1283
+ }
1284
+ out.push(frame);
1285
+ }
1286
+ return out;
1287
+ }
1288
+ //#endregion
1289
+ //#region src/export.ts
1290
+ /**
1291
+ * @glissade/lottie EXPORT (Track → Lottie): a SceneModule → a `LottieDocument`,
1292
+ * the shipped importer (`convert.ts`) read backwards. Every mapping here is the
1293
+ * exact inverse of an import step, so a mappable scene round-trips through
1294
+ * `importLottie` faithfully (see test/roundtrip.test.ts).
1295
+ *
1296
+ * INPUT is the SceneModule (`{ createScene, timeline }`), NOT a flattened
1297
+ * DisplayList: Lottie is an ANIMATED, HIERARCHICAL format, so the node TREE +
1298
+ * the Timeline are the correct source. The walk emits one Lottie layer per node
1299
+ * (Group → a null/ty:3 transform parent; Rect/Circle/Path → a ty:4 shape layer),
1300
+ * parents children via `ind`/`parent`, and turns each `<id>/<prop>` track into an
1301
+ * animated Lottie channel (no track → a static `{a:0,k}` sampled at t=0).
1302
+ *
1303
+ * EASE FIDELITY: cubicBezier + hold + linear eases invert EXACTLY (emitKeyframes).
1304
+ * Named eases, springs, and expr formulas can't be one Lottie bezier — those are
1305
+ * baked to dense linear keys by sampling on the frame grid (sampleFallback), the
1306
+ * same discipline the importer uses for misaligned parametric geometry.
1307
+ *
1308
+ * SCOPE (MVP — mirror the importer's audit discipline: warn + drop, never
1309
+ * silent). IN: Group hierarchy, Rect/Circle/Path with a SOLID fill (+ optional
1310
+ * stroke), transform channels (position / position.x/.y split, opacity, scale,
1311
+ * rotation → identity degrees), animated `fill` color, animated `d` path
1312
+ * (constant topology). OUT (warned + dropped): Text, Image/Video, gradient/mesh
1313
+ * paint (solid only), non-center anchors, group opacity compositing (Lottie
1314
+ * parenting never inherits opacity). Animated primitive geometry (width/radius
1315
+ * tracks) is SAMPLED, not channel-mapped.
1316
+ */
1317
+ const EMPTY_TRACKS = /* @__PURE__ */ new Map();
1318
+ /** Convert a SceneModule to a Lottie document. Pure over (scene, timeline). */
1319
+ function exportLottie(mod, opts) {
1320
+ const scene = mod.createScene();
1321
+ const fr = opts.fps ?? mod.timeline.fps ?? 60;
1322
+ const warn = opts.onWarn ?? ((m) => console.warn(`gs export: ${m}`));
1323
+ const byNode = /* @__PURE__ */ new Map();
1324
+ for (const tr of mod.timeline.tracks) {
1325
+ const resolved = resolveTrackNode(scene.nodes, tr.target);
1326
+ if (resolved === void 0) {
1327
+ warn(`track '${tr.target}' targets no node in the scene — dropped`);
1328
+ continue;
1329
+ }
1330
+ const [nodeId, prop] = resolved;
1331
+ let m = byNode.get(nodeId);
1332
+ if (m === void 0) {
1333
+ m = /* @__PURE__ */ new Map();
1334
+ byNode.set(nodeId, m);
1335
+ }
1336
+ m.set(prop, tr);
1337
+ }
1338
+ const op = computeOp(mod.timeline, fr);
1339
+ const ctx = {
1340
+ fr,
1341
+ ip: 0,
1342
+ op,
1343
+ warn,
1344
+ layers: [],
1345
+ ind: 0
1346
+ };
1347
+ walkChildren(ctx, scene.root.children, void 0, byNode);
1348
+ return {
1349
+ fr,
1350
+ ip: 0,
1351
+ op,
1352
+ w: opts.width,
1353
+ h: opts.height,
1354
+ nm: "glissade export",
1355
+ layers: ctx.layers
1356
+ };
1357
+ }
1358
+ /** Resolve a track target to `[nodeId, propPath]` by the longest registered-id prefix. */
1359
+ function resolveTrackNode(nodes, target) {
1360
+ for (let slash = target.lastIndexOf("/"); slash > 0; slash = target.lastIndexOf("/", slash - 1)) {
1361
+ const id = target.slice(0, slash);
1362
+ if (nodes.has(id)) return [id, target.slice(slash + 1)];
1363
+ }
1364
+ }
1365
+ /** Document out-point: the timeline duration, else the max key time (≥ 1 frame). */
1366
+ function computeOp(tl, fr) {
1367
+ if (tl.duration !== void 0) return Math.max(1, Math.round(tl.duration * fr));
1368
+ let maxT = 0;
1369
+ for (const tr of tl.tracks) {
1370
+ const last = tr.keys[tr.keys.length - 1];
1371
+ if (last) maxT = Math.max(maxT, last.t);
1372
+ }
1373
+ return Math.max(1, Math.round(maxT * fr));
1374
+ }
1375
+ /**
1376
+ * Emit a sibling list in REVERSE array order so the array-LAST (top-painted)
1377
+ * node gets the SMALLER `ind` — the importer reconstructs paint order from
1378
+ * `zIndex = -ind`, so a descending ind per sibling group preserves it.
1379
+ */
1380
+ function walkChildren(ctx, children, parentInd, byNode) {
1381
+ for (let i = children.length - 1; i >= 0; i--) {
1382
+ const node = children[i];
1383
+ const kind = classify(node);
1384
+ if (kind === "drop") {
1385
+ ctx.warn(`${describe(node)} is not exportable (MVP: Group / Rect / Circle / Path) — dropped`);
1386
+ continue;
1387
+ }
1388
+ const myInd = ++ctx.ind;
1389
+ const tracks = (node.id !== void 0 ? byNode.get(node.id) : void 0) ?? EMPTY_TRACKS;
1390
+ ctx.layers.push(kind === "group" ? buildNullLayer(ctx, node, myInd, parentInd, tracks) : buildShapeLayer(ctx, node, kind, myInd, parentInd, tracks));
1391
+ if (node instanceof Group) walkChildren(ctx, node.children, myInd, byNode);
1392
+ }
1393
+ }
1394
+ function classify(node) {
1395
+ if (node instanceof Rect) return "rect";
1396
+ if (node instanceof Circle) return "circle";
1397
+ if (node instanceof Path) return "path";
1398
+ if (node instanceof Group) return "group";
1399
+ return "drop";
1400
+ }
1401
+ const describe = (node) => `${node.describeType}${node.id !== void 0 ? ` '${node.id}'` : ""}`;
1402
+ function buildTransform(ctx, node, tracks) {
1403
+ 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`);
1404
+ return {
1405
+ a: {
1406
+ a: 0,
1407
+ k: [0, 0]
1408
+ },
1409
+ p: positionProp(ctx, tracks, node.position()),
1410
+ s: vecProp(ctx, tracks, "scale", node.scale(), (v) => [v[0] * 100, v[1] * 100]),
1411
+ r: scalarProp(ctx, tracks, "rotation", node.rotation(), (v) => v),
1412
+ o: scalarProp(ctx, tracks, "opacity", node.opacity(), (v) => v * 100)
1413
+ };
1414
+ }
1415
+ function scalarProp(ctx, tracks, prop, staticVal, map) {
1416
+ const tr = tracks.get(prop);
1417
+ if (tr) return {
1418
+ a: 1,
1419
+ k: scalarKeys(ctx, tr, map)
1420
+ };
1421
+ return {
1422
+ a: 0,
1423
+ k: map(staticVal)
1424
+ };
1425
+ }
1426
+ function scalarKeys(ctx, tr, map) {
1427
+ const toS = (v) => [map(v)];
1428
+ return isDirectlyInvertible(tr.keys, tr.expr) ? emitKeys(tr.keys, ctx.fr, toS) : sampleToLottieKeys(tr, ctx.fr, ctx.ip, ctx.op, toS);
1429
+ }
1430
+ function vecProp(ctx, tracks, prop, staticVal, map) {
1431
+ const whole = tracks.get(prop);
1432
+ if (whole) return {
1433
+ a: 1,
1434
+ k: isDirectlyInvertible(whole.keys, whole.expr) ? emitKeys(whole.keys, ctx.fr, map) : sampleToLottieKeys(whole, ctx.fr, ctx.ip, ctx.op, map)
1435
+ };
1436
+ const xt = tracks.get(`${prop}.x`);
1437
+ const yt = tracks.get(`${prop}.y`);
1438
+ if (xt || yt) {
1439
+ ctx.warn(`per-axis '${prop}' animation is sampled at ${ctx.fr} fps into a combined channel`);
1440
+ return {
1441
+ a: 1,
1442
+ k: sampleComponentVec(ctx, xt, yt, staticVal, map)
1443
+ };
1444
+ }
1445
+ return {
1446
+ a: 0,
1447
+ k: map(staticVal)
1448
+ };
1449
+ }
1450
+ /** Position supports Lottie's native split form (`{s:true,x,y}`) for exactness. */
1451
+ function positionProp(ctx, tracks, staticPos) {
1452
+ const whole = tracks.get("position");
1453
+ if (whole) {
1454
+ const toS = (v) => [v[0], v[1]];
1455
+ return {
1456
+ a: 1,
1457
+ k: isDirectlyInvertible(whole.keys, whole.expr) ? emitKeys(whole.keys, ctx.fr, toS) : sampleToLottieKeys(whole, ctx.fr, ctx.ip, ctx.op, toS)
1458
+ };
1459
+ }
1460
+ const xt = tracks.get("position.x");
1461
+ const yt = tracks.get("position.y");
1462
+ if (xt || yt) return {
1463
+ s: true,
1464
+ x: xt ? {
1465
+ a: 1,
1466
+ k: scalarKeys(ctx, xt, (v) => v)
1467
+ } : {
1468
+ a: 0,
1469
+ k: staticPos[0]
1470
+ },
1471
+ y: yt ? {
1472
+ a: 1,
1473
+ k: scalarKeys(ctx, yt, (v) => v)
1474
+ } : {
1475
+ a: 0,
1476
+ k: staticPos[1]
1477
+ }
1478
+ };
1479
+ return {
1480
+ a: 0,
1481
+ k: [staticPos[0], staticPos[1]]
1482
+ };
1483
+ }
1484
+ function sampleComponentVec(ctx, xt, yt, staticVal, map) {
1485
+ const [f0, f1] = frameSpan(ctx, [xt, yt]);
1486
+ const out = [];
1487
+ for (let f = f0; f <= f1; f++) {
1488
+ const t = f / ctx.fr;
1489
+ const x = xt ? sampleTrack(xt, t) : staticVal[0];
1490
+ const y = yt ? sampleTrack(yt, t) : staticVal[1];
1491
+ const frame = {
1492
+ t: f,
1493
+ s: map([x, y])
1494
+ };
1495
+ if (f < f1) {
1496
+ frame.o = {
1497
+ x: 0,
1498
+ y: 0
1499
+ };
1500
+ frame.i = {
1501
+ x: 1,
1502
+ y: 1
1503
+ };
1504
+ }
1505
+ out.push(frame);
1506
+ }
1507
+ return out;
1508
+ }
1509
+ /** Union frame span of a set of tracks (their first→last key), else [ip, op]. */
1510
+ function frameSpan(ctx, tracks) {
1511
+ const bounds = [];
1512
+ 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));
1513
+ return bounds.length > 0 ? [Math.min(...bounds), Math.max(...bounds)] : [ctx.ip, ctx.op];
1514
+ }
1515
+ function buildNullLayer(ctx, node, ind, parentInd, tracks) {
1516
+ 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`);
1517
+ return {
1518
+ ty: 3,
1519
+ nm: node.id ?? `group${ind}`,
1520
+ ind,
1521
+ ip: ctx.ip,
1522
+ op: ctx.op,
1523
+ ks: buildTransform(ctx, node, tracks),
1524
+ ...parentInd !== void 0 ? { parent: parentInd } : {}
1525
+ };
1526
+ }
1527
+ function buildShapeLayer(ctx, node, kind, ind, parentInd, tracks) {
1528
+ const shapes = [buildGeometry(ctx, node, kind, tracks)];
1529
+ const stroke = buildStroke(ctx, node, tracks);
1530
+ if (stroke) shapes.push(stroke);
1531
+ const fill = buildFill(ctx, node, tracks);
1532
+ if (fill) shapes.push(fill);
1533
+ return {
1534
+ ty: 4,
1535
+ nm: node.id ?? `shape${ind}`,
1536
+ ind,
1537
+ ip: ctx.ip,
1538
+ op: ctx.op,
1539
+ ks: buildTransform(ctx, node, tracks),
1540
+ shapes,
1541
+ ...parentInd !== void 0 ? { parent: parentInd } : {}
1542
+ };
1543
+ }
1544
+ function buildGeometry(ctx, node, kind, tracks) {
1545
+ if (kind === "path") return buildPathGeometry(ctx, node, tracks);
1546
+ const paramNames = kind === "rect" ? [
1547
+ "width",
1548
+ "height",
1549
+ "cornerRadius"
1550
+ ] : ["radius"];
1551
+ const contourAt = (t) => {
1552
+ if (kind === "rect") {
1553
+ const r = node;
1554
+ return rectContour([0, 0], [paramAt(tracks, "width", r.width(), t), paramAt(tracks, "height", r.height(), t)], paramAt(tracks, "cornerRadius", r.cornerRadius(), t));
1555
+ }
1556
+ const rad = paramAt(tracks, "radius", node.radius(), t);
1557
+ return ellipseContour([0, 0], [rad * 2, rad * 2]);
1558
+ };
1559
+ if (!paramNames.some((p) => tracks.has(p))) return {
1560
+ ty: "sh",
1561
+ ks: {
1562
+ a: 0,
1563
+ k: [contourToShData(contourAt(0))]
1564
+ }
1565
+ };
1566
+ ctx.warn(`${describe(node)}: animated primitive geometry is sampled at ${ctx.fr} fps (not channel-mapped)`);
1567
+ const [f0, f1] = frameSpan(ctx, paramNames.map((p) => tracks.get(p)));
1568
+ const keys = [];
1569
+ for (let f = f0; f <= f1; f++) {
1570
+ const frame = {
1571
+ t: f,
1572
+ s: [contourToShData(contourAt(f / ctx.fr))]
1573
+ };
1574
+ if (f < f1) {
1575
+ frame.o = {
1576
+ x: 0,
1577
+ y: 0
1578
+ };
1579
+ frame.i = {
1580
+ x: 1,
1581
+ y: 1
1582
+ };
1583
+ }
1584
+ keys.push(frame);
1585
+ }
1586
+ return {
1587
+ ty: "sh",
1588
+ ks: {
1589
+ a: 1,
1590
+ k: keys
1591
+ }
1592
+ };
1593
+ }
1594
+ function paramAt(tracks, prop, staticVal, t) {
1595
+ const tr = tracks.get(prop);
1596
+ return tr ? sampleTrack(tr, t) : staticVal;
1597
+ }
1598
+ function buildPathGeometry(ctx, node, tracks) {
1599
+ const tr = tracks.get("d");
1600
+ if (tr) return {
1601
+ ty: "sh",
1602
+ ks: {
1603
+ a: 1,
1604
+ k: isDirectlyInvertible(tr.keys, tr.expr) ? emitKeys(tr.keys, ctx.fr, pathValueToShData) : sampleToLottieKeys(tr, ctx.fr, ctx.ip, ctx.op, pathValueToShData)
1605
+ }
1606
+ };
1607
+ return {
1608
+ ty: "sh",
1609
+ ks: {
1610
+ a: 0,
1611
+ k: pathValueToShData(node.data())
1612
+ }
1613
+ };
1614
+ }
1615
+ function colorToLottie(css) {
1616
+ const { r, g, b, a } = parseColor(css);
1617
+ const base = [
1618
+ r / 255,
1619
+ g / 255,
1620
+ b / 255
1621
+ ];
1622
+ return a >= 1 ? base : [...base, a];
1623
+ }
1624
+ function colorKeys(ctx, tr) {
1625
+ return isDirectlyInvertible(tr.keys, tr.expr) ? emitKeys(tr.keys, ctx.fr, colorToLottie) : sampleToLottieKeys(tr, ctx.fr, ctx.ip, ctx.op, colorToLottie);
1626
+ }
1627
+ function buildFill(ctx, node, tracks) {
1628
+ const tr = tracks.get("fill");
1629
+ if (tr) {
1630
+ if (tr.type !== "color") {
1631
+ ctx.warn(`${describe(node)}: animated '${tr.type}' fill (gradient/mesh) is not exported — dropped`);
1632
+ return;
1633
+ }
1634
+ return {
1635
+ ty: "fl",
1636
+ c: {
1637
+ a: 1,
1638
+ k: colorKeys(ctx, tr)
1639
+ },
1640
+ o: {
1641
+ a: 0,
1642
+ k: 100
1643
+ }
1644
+ };
1645
+ }
1646
+ const fill = node.fill();
1647
+ if (typeof fill !== "string") {
1648
+ ctx.warn(`${describe(node)}: a gradient/mesh fill is not exported (MVP: solid color) — dropped`);
1649
+ return;
1650
+ }
1651
+ if (fill === "") return void 0;
1652
+ return {
1653
+ ty: "fl",
1654
+ c: {
1655
+ a: 0,
1656
+ k: colorToLottie(fill)
1657
+ },
1658
+ o: {
1659
+ a: 0,
1660
+ k: 100
1661
+ }
1662
+ };
1663
+ }
1664
+ function buildStroke(ctx, node, tracks) {
1665
+ const colorTr = tracks.get("stroke");
1666
+ const widthTr = tracks.get("strokeWidth");
1667
+ const staticStroke = node.stroke();
1668
+ const staticWidth = node.strokeWidth();
1669
+ if (!(colorTr !== void 0 || staticStroke !== "") || !(widthTr !== void 0 || staticWidth > 0)) return void 0;
1670
+ return {
1671
+ ty: "st",
1672
+ c: colorTr ? {
1673
+ a: 1,
1674
+ k: colorKeys(ctx, colorTr)
1675
+ } : {
1676
+ a: 0,
1677
+ k: colorToLottie(staticStroke)
1678
+ },
1679
+ o: {
1680
+ a: 0,
1681
+ k: 100
1682
+ },
1683
+ w: widthTr ? {
1684
+ a: 1,
1685
+ k: scalarKeys(ctx, widthTr, (v) => v)
1686
+ } : {
1687
+ a: 0,
1688
+ k: staticWidth
1689
+ }
1690
+ };
1691
+ }
1692
+ //#endregion
1151
1693
  //#region src/index.ts
1152
1694
  function assertDocument(json) {
1153
1695
  const doc = json;
@@ -1172,4 +1714,4 @@ function importLottie(json, opts = {}) {
1172
1714
  };
1173
1715
  }
1174
1716
  //#endregion
1175
- export { KAPPA, LottieImportError, buildNode, buildNodes, colorPropIsBytes, ellipseContour, generateSceneModule, importLottie, lottieColor, mergeContours, rectContour, reverseContour, shToContour };
1717
+ 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-pre.1",
3
+ "version": "0.45.0-pre.0",
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-pre.1",
22
- "@glissade/scene": "0.44.0-pre.1"
21
+ "@glissade/core": "0.45.0-pre.0",
22
+ "@glissade/scene": "0.45.0-pre.0"
23
+ },
24
+ "devDependencies": {
25
+ "@glissade/backend-skia": "0.45.0-pre.0"
23
26
  },
24
27
  "repository": {
25
28
  "type": "git",