@glissade/lottie 0.53.0-pre.0 → 0.54.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
@@ -320,6 +320,15 @@ interface ExportOptions {
320
320
  * wouldn't match a faithful render, so we don't bake with it).
321
321
  */
322
322
  measurer?: TextMeasurer;
323
+ /**
324
+ * PNG encoder threaded from the CLI (like `measurer`) so a MESH fill can be
325
+ * rasterized (the pure `rasterizeMesh` kernel) and embedded as a ty:2 image
326
+ * layer — Lottie has no mesh primitive. `@glissade/lottie` stays DOM/Node-free,
327
+ * so it can't encode PNG itself. Returns BASE64 (no `data:` prefix; the exporter
328
+ * prepends `data:image/png;base64,`). ABSENT (pure-JS callers/tests) → a mesh
329
+ * fill keeps the historical warn-drop, so non-CLI exports are unchanged.
330
+ */
331
+ encodePng?: (rgba: Uint8ClampedArray, w: number, h: number) => string;
323
332
  }
324
333
  /** Convert a SceneModule to a Lottie document. Pure over (scene, timeline). */
325
334
  declare function exportLottie(mod: SceneModule, opts: ExportOptions): LottieDocument;
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Circle, Group, ImageNode, Path, Rect, Text, breakLines, createScene } from "@glissade/scene";
1
+ import { Circle, Group, ImageNode, Path, Rect, Text, breakLines, createScene, meshRasterSize, rasterizeMesh } from "@glissade/scene";
2
2
  import { compileTimeline, cubicBezier, formatColor, parseColor, sampleTrack, track } from "@glissade/core";
3
3
  import "@glissade/core/expr";
4
4
  //#region src/spec.ts
@@ -1733,7 +1733,9 @@ function exportLottie(mod, opts) {
1733
1733
  layers: [],
1734
1734
  ind: 0,
1735
1735
  fonts: /* @__PURE__ */ new Map(),
1736
- measurer: opts.measurer
1736
+ measurer: opts.measurer,
1737
+ encodePng: opts.encodePng,
1738
+ assets: []
1737
1739
  };
1738
1740
  walkChildren(ctx, scene.root.children, void 0, byNode, IDENTITY_OPACITY);
1739
1741
  const fonts = [...ctx.fonts.values()];
@@ -1746,6 +1748,7 @@ function exportLottie(mod, opts) {
1746
1748
  h: opts.height,
1747
1749
  nm: "glissade export",
1748
1750
  layers: ctx.layers,
1751
+ ...ctx.assets.length > 0 ? { assets: ctx.assets } : {},
1749
1752
  ...fonts.length > 0 ? { fonts: { list: fonts } } : {}
1750
1753
  };
1751
1754
  }
@@ -2026,7 +2029,7 @@ function buildShapeLayer(ctx, node, kind, ind, parentInd, tracks, opacity) {
2026
2029
  const shapes = [buildGeometry(ctx, node, kind, tracks)];
2027
2030
  const stroke = buildStroke(ctx, node, tracks);
2028
2031
  if (stroke) shapes.push(stroke);
2029
- const fill = buildFill(ctx, node, tracks);
2032
+ const fill = buildFill(ctx, node, tracks, ind);
2030
2033
  if (fill) shapes.push(fill);
2031
2034
  return {
2032
2035
  ty: 4,
@@ -2267,7 +2270,7 @@ function colorToLottie(css) {
2267
2270
  function colorKeys(ctx, tr) {
2268
2271
  return isDirectlyInvertible(tr.keys, tr.expr) ? emitKeys(tr.keys, ctx.fr, colorToLottie) : sampleToLottieKeys(tr, ctx.fr, ctx.ip, ctx.op, colorToLottie);
2269
2272
  }
2270
- function buildFill(ctx, node, tracks) {
2273
+ function buildFill(ctx, node, tracks, ind) {
2271
2274
  const tr = tracks.get("fill");
2272
2275
  if (tr) {
2273
2276
  if (tr.type === "color") return {
@@ -2281,7 +2284,7 @@ function buildFill(ctx, node, tracks) {
2281
2284
  k: 100
2282
2285
  }
2283
2286
  };
2284
- if (tr.type === "paint") return buildAnimatedGradientFill(ctx, node, tr);
2287
+ if (tr.type === "paint") return buildAnimatedGradientFill(ctx, node, tr, ind);
2285
2288
  ctx.warn(`${describe(node)}: animated '${tr.type}' fill is not exported — dropped`);
2286
2289
  return;
2287
2290
  }
@@ -2312,12 +2315,80 @@ function buildFill(ctx, node, tracks) {
2312
2315
  }
2313
2316
  };
2314
2317
  if (fill.kind === "mesh") {
2318
+ if (ctx.encodePng) {
2319
+ emitMeshRaster(ctx, node, fill, ind);
2320
+ return;
2321
+ }
2315
2322
  ctx.warn(`${describe(node)}: a mesh fill has no Lottie gradient ramp (MVP: solid / linear / radial) — dropped`);
2316
2323
  return;
2317
2324
  }
2318
2325
  warnGradientInterpolation(ctx, node, fill);
2319
2326
  return gradientFillItem(fill, localBounds(node));
2320
2327
  }
2328
+ /**
2329
+ * Rasterize a STATIC (or first-key-flattened) mesh Paint to a PNG and emit it as a
2330
+ * ty:2 image LAYER parented to the shape layer (`shapeInd`). The raster's placement
2331
+ * mirrors raster2d.fillMesh's blit rect EXACTLY: the buffer covers the fill's
2332
+ * `localBounds`, so expressing the image in SHAPE-LOCAL coordinates (anchor [0,0],
2333
+ * position = bounds top-left, scale = bounds/raster) lets the shape layer's own
2334
+ * transform (position/rotation/scale/anchor) carry it to screen — the importer
2335
+ * re-parents the image under the shape's transform group, aligning it with the
2336
+ * (now fill-less) geometry. Gated on `ctx.encodePng` by the caller. Deterministic:
2337
+ * `rasterizeMesh` is pure, the Skia PNG encode is byte-stable, base64 is total.
2338
+ */
2339
+ function emitMeshRaster(ctx, node, mesh, shapeInd) {
2340
+ const b = localBounds(node);
2341
+ const bw = b.maxX - b.minX;
2342
+ const bh = b.maxY - b.minY;
2343
+ if (bw <= 0 || bh <= 0) {
2344
+ ctx.warn(`${describe(node)}: a mesh fill has empty local bounds — dropped`);
2345
+ return;
2346
+ }
2347
+ const { w: rw, h: rh } = meshRasterSize(bw, bh);
2348
+ const rgba = rasterizeMesh(mesh, rw, rh);
2349
+ const b64 = ctx.encodePng(rgba, rw, rh);
2350
+ const id = `mesh_${ctx.assets.length}`;
2351
+ ctx.assets.push({
2352
+ id,
2353
+ w: rw,
2354
+ h: rh,
2355
+ u: "",
2356
+ p: `data:image/png;base64,${b64}`,
2357
+ e: 1
2358
+ });
2359
+ const layerInd = ++ctx.ind;
2360
+ ctx.layers.push({
2361
+ ty: 2,
2362
+ nm: `${node.id ?? `mesh${shapeInd}`}_raster`,
2363
+ refId: id,
2364
+ ind: layerInd,
2365
+ ip: ctx.ip,
2366
+ op: ctx.op,
2367
+ ks: {
2368
+ a: {
2369
+ a: 0,
2370
+ k: [0, 0]
2371
+ },
2372
+ p: {
2373
+ a: 0,
2374
+ k: [b.minX, b.minY]
2375
+ },
2376
+ s: {
2377
+ a: 0,
2378
+ k: [bw / rw * 100, bh / rh * 100]
2379
+ },
2380
+ r: {
2381
+ a: 0,
2382
+ k: 0
2383
+ },
2384
+ o: {
2385
+ a: 0,
2386
+ k: 100
2387
+ }
2388
+ },
2389
+ parent: shapeInd
2390
+ });
2391
+ }
2321
2392
  /** Local-space bounds of a shape's fill path — the gradient-geometry default source
2322
2393
  * (matches raster2d.resolveFill: linear ⇒ vertical bounds sweep, radial ⇒ centre +
2323
2394
  * half-diagonal). Rect/Circle draw centred at the origin; a Path bounds its anchors. */
@@ -2461,9 +2532,14 @@ function gradientFillItem(g, bounds) {
2461
2532
  * the frame grid. A key whose kind/stop-count differs from the first would snap
2462
2533
  * under paintType anyway, so its geometry/ramp is read as best-effort.
2463
2534
  */
2464
- function buildAnimatedGradientFill(ctx, node, tr) {
2535
+ function buildAnimatedGradientFill(ctx, node, tr, ind) {
2465
2536
  const first = tr.keys[0].value;
2466
2537
  if (first.kind === "mesh") {
2538
+ if (ctx.encodePng) {
2539
+ ctx.warn(`${describe(node)}: mesh animation is flattened to a static raster (first key) — motion dropped`);
2540
+ emitMeshRaster(ctx, node, first, ind);
2541
+ return;
2542
+ }
2467
2543
  ctx.warn(`${describe(node)}: an animated mesh fill has no Lottie gradient ramp — dropped`);
2468
2544
  return;
2469
2545
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glissade/lottie",
3
- "version": "0.53.0-pre.0",
3
+ "version": "0.54.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,11 +18,11 @@
18
18
  "dist"
19
19
  ],
20
20
  "dependencies": {
21
- "@glissade/core": "0.53.0-pre.0",
22
- "@glissade/scene": "0.53.0-pre.0"
21
+ "@glissade/core": "0.54.0-pre.0",
22
+ "@glissade/scene": "0.54.0-pre.0"
23
23
  },
24
24
  "devDependencies": {
25
- "@glissade/backend-skia": "0.53.0-pre.0"
25
+ "@glissade/backend-skia": "0.54.0-pre.0"
26
26
  },
27
27
  "repository": {
28
28
  "type": "git",