@glissade/lottie 0.47.0 → 0.48.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.
Files changed (2) hide show
  1. package/dist/index.js +106 -16
  2. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Circle, Group, ImageNode, Path, Rect, Text, createScene } from "@glissade/scene";
2
- import { cubicBezier, formatColor, parseColor, sampleTrack, track } from "@glissade/core";
2
+ import { compileTimeline, cubicBezier, formatColor, parseColor, sampleTrack, track } from "@glissade/core";
3
3
  import "@glissade/core/expr";
4
4
  //#region src/spec.ts
5
5
  var LottieImportError = class extends Error {
@@ -1500,21 +1500,40 @@ function decimateLinearKeys(keys, relEps = .002) {
1500
1500
  * (constant topology), and TEXT (ty:5): a Text node → a text layer with a font
1501
1501
  * reference (fonts.list) + a text-document keyframe stream (static = one doc;
1502
1502
  * animated text/fill/fontSize → doc keyframes sampled on the frame grid, held).
1503
+ *
1504
+ * GROUP OPACITY (baked into descendants): Lottie null-parent parenting inherits
1505
+ * the transform MATRIX only, never opacity, so a `Group{opacity<1}` (or an
1506
+ * animated `opacity` track) is composited by MULTIPLYING its opacity into every
1507
+ * LEAF descendant's `ks.o` (static → a product; animated → the product sampled on
1508
+ * the frame grid + decimated, the sampleComponentVec discipline) while the group
1509
+ * null keeps its p/r/s and carries `o:{a:0,k:100}`. This is EXACT when a group's
1510
+ * translucent descendants DON'T overlap (or the group is near-0/near-1 — the
1511
+ * reported leak-through case: a group fading to ~0 hides its children since
1512
+ * 0×anything≈0). LIMIT (warned, never silent): OVERLAPPING translucent siblings
1513
+ * double-composite — glissade composites the subtree as a unit then applies the
1514
+ * group alpha once, whereas per-child baking stacks the alphas (0.5 over 0.5 ≈
1515
+ * 0.75, not the correct single 0.5). Same limitation the importer documents at
1516
+ * convert.ts:470-475. A correct-for-overlap precomp (ty:0 + assets) is a later phase.
1517
+ *
1503
1518
  * OUT (warned + dropped): Image/Video, gradient/mesh paint (solid only),
1504
- * non-center anchors, group opacity compositing (Lottie parenting never inherits
1505
- * opacity), text typewriter `reveal`/`revealFraction`, variable-font axes
1519
+ * non-center anchors, text typewriter `reveal`/`revealFraction`, variable-font axes
1506
1520
  * (`fontAxes`/`fontVariationSettings` — no Lottie doc field), `box` valign
1507
1521
  * (baseline-approximated) and wrap `width` (the player self-reflows), TokenHighlight.
1508
1522
  * Animated primitive geometry (width/radius tracks) is SAMPLED, not channel-mapped.
1509
1523
  */
1510
1524
  const EMPTY_TRACKS = /* @__PURE__ */ new Map();
1525
+ const IDENTITY_OPACITY = {
1526
+ factor: 1,
1527
+ tracks: []
1528
+ };
1511
1529
  /** Convert a SceneModule to a Lottie document. Pure over (scene, timeline). */
1512
1530
  function exportLottie(mod, opts) {
1513
1531
  const scene = mod.createScene();
1514
1532
  const fr = opts.fps ?? mod.timeline.fps ?? 60;
1515
1533
  const warn = opts.onWarn ?? ((m) => console.warn(`gs export: ${m}`));
1534
+ const compiled = compileTimeline(mod.timeline);
1516
1535
  const byNode = /* @__PURE__ */ new Map();
1517
- for (const tr of mod.timeline.tracks) {
1536
+ for (const tr of compiled.tracks.values()) {
1518
1537
  const resolved = resolveTrackNode(scene.nodes, tr.target);
1519
1538
  if (resolved === void 0) {
1520
1539
  warn(`track '${tr.target}' targets no node in the scene — dropped`);
@@ -1538,7 +1557,7 @@ function exportLottie(mod, opts) {
1538
1557
  ind: 0,
1539
1558
  fonts: /* @__PURE__ */ new Map()
1540
1559
  };
1541
- walkChildren(ctx, scene.root.children, void 0, byNode);
1560
+ walkChildren(ctx, scene.root.children, void 0, byNode, IDENTITY_OPACITY);
1542
1561
  const fonts = [...ctx.fonts.values()];
1543
1562
  return {
1544
1563
  v: BODYMOVIN_VERSION,
@@ -1580,7 +1599,7 @@ function computeOp(tl, fr) {
1580
1599
  * node gets the SMALLER `ind` — the importer reconstructs paint order from
1581
1600
  * `zIndex = -ind`, so a descending ind per sibling group preserves it.
1582
1601
  */
1583
- function walkChildren(ctx, children, parentInd, byNode) {
1602
+ function walkChildren(ctx, children, parentInd, byNode, opacity) {
1584
1603
  for (let i = children.length - 1; i >= 0; i--) {
1585
1604
  const node = children[i];
1586
1605
  const kind = classify(node);
@@ -1590,10 +1609,34 @@ function walkChildren(ctx, children, parentInd, byNode) {
1590
1609
  }
1591
1610
  const myInd = ++ctx.ind;
1592
1611
  const tracks = (node.id !== void 0 ? byNode.get(node.id) : void 0) ?? EMPTY_TRACKS;
1593
- ctx.layers.push(kind === "group" ? buildNullLayer(ctx, node, myInd, parentInd, tracks) : kind === "text" ? buildTextLayer(ctx, node, myInd, parentInd, tracks) : buildShapeLayer(ctx, node, kind, myInd, parentInd, tracks));
1594
- if (node instanceof Group) walkChildren(ctx, node.children, myInd, byNode);
1612
+ ctx.layers.push(kind === "group" ? buildNullLayer(ctx, node, myInd, parentInd, tracks) : kind === "text" ? buildTextLayer(ctx, node, myInd, parentInd, tracks, opacity) : buildShapeLayer(ctx, node, kind, myInd, parentInd, tracks, opacity));
1613
+ if (node instanceof Group) walkChildren(ctx, node.children, myInd, byNode, childOpacity(node, tracks, opacity));
1595
1614
  }
1596
1615
  }
1616
+ /**
1617
+ * The opacity accumulator for a group's children: a group WITH an opacity track
1618
+ * contributes that track (sampled per-frame); one WITHOUT multiplies its static
1619
+ * opacity into `factor`. The group's own opacity is thus pushed down onto its
1620
+ * descendants (the null layer itself carries `o:100`).
1621
+ */
1622
+ function childOpacity(group, tracks, parent) {
1623
+ const track = tracks.get("opacity");
1624
+ if (track) return {
1625
+ factor: parent.factor,
1626
+ tracks: [...parent.tracks, track]
1627
+ };
1628
+ return {
1629
+ factor: parent.factor * group.opacity(),
1630
+ tracks: parent.tracks
1631
+ };
1632
+ }
1633
+ /** Count exportable LEAF (non-group, non-drop) descendants — the overlap-warn heuristic. */
1634
+ function countLeafDescendants(group) {
1635
+ let n = 0;
1636
+ for (const child of group.children) if (child instanceof Group) n += countLeafDescendants(child);
1637
+ else if (classify(child) !== "drop") n += 1;
1638
+ return n;
1639
+ }
1597
1640
  function classify(node) {
1598
1641
  if (node instanceof Rect) return "rect";
1599
1642
  if (node instanceof Circle) return "circle";
@@ -1603,7 +1646,7 @@ function classify(node) {
1603
1646
  return "drop";
1604
1647
  }
1605
1648
  const describe = (node) => `${node.describeType}${node.id !== void 0 ? ` '${node.id}'` : ""}`;
1606
- function buildTransform(ctx, node, tracks) {
1649
+ function buildTransform(ctx, node, tracks, o) {
1607
1650
  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`);
1608
1651
  return {
1609
1652
  a: {
@@ -1613,7 +1656,51 @@ function buildTransform(ctx, node, tracks) {
1613
1656
  p: positionProp(ctx, tracks, node.position()),
1614
1657
  s: vecProp(ctx, tracks, "scale", node.scale(), (v) => [v[0] * 100, v[1] * 100]),
1615
1658
  r: scalarProp(ctx, tracks, "rotation", node.rotation(), (v) => v),
1616
- o: scalarProp(ctx, tracks, "opacity", node.opacity(), (v) => v * 100)
1659
+ o
1660
+ };
1661
+ }
1662
+ /**
1663
+ * A leaf's `ks.o`, folding in the group opacity accumulated from its ancestors.
1664
+ * With NO ancestor contribution (identity accumulator) this is byte-identical to
1665
+ * the pre-feature `scalarProp` path — the exact cubicBezier/hold inversion is
1666
+ * preserved. A static-only accumulator multiplies into a single `{a:0,k}`.
1667
+ * Anything animated (a leaf `opacity` track and/or an ancestor track) samples
1668
+ * the product `leaf_opacity(t) × Π ancestor_opacity(t)` on the union frame span
1669
+ * and decimates — the identical discipline sampleComponentVec uses.
1670
+ */
1671
+ function combineOpacity(ctx, node, tracks, accum) {
1672
+ const leafTrack = tracks.get("opacity");
1673
+ const leafStatic = node.opacity();
1674
+ if (accum.factor === 1 && accum.tracks.length === 0) return scalarProp(ctx, tracks, "opacity", leafStatic, (v) => v * 100);
1675
+ if (leafTrack === void 0 && accum.tracks.length === 0) return {
1676
+ a: 0,
1677
+ k: leafStatic * accum.factor * 100
1678
+ };
1679
+ const [f0, f1] = frameSpan(ctx, leafTrack ? [leafTrack, ...accum.tracks] : [...accum.tracks]);
1680
+ const out = [];
1681
+ for (let f = f0; f <= f1; f++) {
1682
+ const t = f / ctx.fr;
1683
+ let product = (leafTrack ? sampleTrack(leafTrack, t) : leafStatic) * accum.factor;
1684
+ for (const at of accum.tracks) product *= sampleTrack(at, t);
1685
+ const frame = {
1686
+ t: f,
1687
+ s: [product * 100]
1688
+ };
1689
+ if (f < f1) {
1690
+ frame.o = {
1691
+ x: 0,
1692
+ y: 0
1693
+ };
1694
+ frame.i = {
1695
+ x: 1,
1696
+ y: 1
1697
+ };
1698
+ }
1699
+ out.push(frame);
1700
+ }
1701
+ return {
1702
+ a: 1,
1703
+ k: decimateLinearKeys(out)
1617
1704
  };
1618
1705
  }
1619
1706
  function scalarProp(ctx, tracks, prop, staticVal, map) {
@@ -1717,18 +1804,21 @@ function frameSpan(ctx, tracks) {
1717
1804
  return bounds.length > 0 ? [Math.min(...bounds), Math.max(...bounds)] : [ctx.ip, ctx.op];
1718
1805
  }
1719
1806
  function buildNullLayer(ctx, node, ind, parentInd, tracks) {
1720
- 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`);
1807
+ if ((node.opacity() !== 1 || tracks.has("opacity")) && node instanceof Group && countLeafDescendants(node) >= 2) ctx.warn(`${describe(node)}: group opacity is baked into descendant leaves; OVERLAPPING translucent descendants may double-composite (exact when they don't overlap)`);
1721
1808
  return {
1722
1809
  ty: 3,
1723
1810
  nm: node.id ?? `group${ind}`,
1724
1811
  ind,
1725
1812
  ip: ctx.ip,
1726
1813
  op: ctx.op,
1727
- ks: buildTransform(ctx, node, tracks),
1814
+ ks: buildTransform(ctx, node, tracks, {
1815
+ a: 0,
1816
+ k: 100
1817
+ }),
1728
1818
  ...parentInd !== void 0 ? { parent: parentInd } : {}
1729
1819
  };
1730
1820
  }
1731
- function buildShapeLayer(ctx, node, kind, ind, parentInd, tracks) {
1821
+ function buildShapeLayer(ctx, node, kind, ind, parentInd, tracks, opacity) {
1732
1822
  const shapes = [buildGeometry(ctx, node, kind, tracks)];
1733
1823
  const stroke = buildStroke(ctx, node, tracks);
1734
1824
  if (stroke) shapes.push(stroke);
@@ -1740,7 +1830,7 @@ function buildShapeLayer(ctx, node, kind, ind, parentInd, tracks) {
1740
1830
  ind,
1741
1831
  ip: ctx.ip,
1742
1832
  op: ctx.op,
1743
- ks: buildTransform(ctx, node, tracks),
1833
+ ks: buildTransform(ctx, node, tracks, combineOpacity(ctx, node, tracks, opacity)),
1744
1834
  shapes,
1745
1835
  ...parentInd !== void 0 ? { parent: parentInd } : {}
1746
1836
  };
@@ -1854,7 +1944,7 @@ function warnTextUnsupported(ctx, node, tracks) {
1854
1944
  if (node.box !== void 0) ctx.warn(`${describe(node)}: box valign is approximated as baseline-anchored (no Lottie ink-box anchor) — vertical placement may shift`);
1855
1945
  if (tracks.has("width") || node.width() > 0) ctx.warn(`${describe(node)}: wrap 'width' relies on the player's own line reflow — wrapping may diverge from glissade's`);
1856
1946
  }
1857
- function buildTextLayer(ctx, node, ind, parentInd, tracks) {
1947
+ function buildTextLayer(ctx, node, ind, parentInd, tracks, opacity) {
1858
1948
  const fName = registerFont(ctx, node);
1859
1949
  warnTextUnsupported(ctx, node, tracks);
1860
1950
  const t = {
@@ -1867,7 +1957,7 @@ function buildTextLayer(ctx, node, ind, parentInd, tracks) {
1867
1957
  ind,
1868
1958
  ip: ctx.ip,
1869
1959
  op: ctx.op,
1870
- ks: buildTransform(ctx, node, tracks),
1960
+ ks: buildTransform(ctx, node, tracks, combineOpacity(ctx, node, tracks, opacity)),
1871
1961
  t,
1872
1962
  ...parentInd !== void 0 ? { parent: parentInd } : {}
1873
1963
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glissade/lottie",
3
- "version": "0.47.0",
3
+ "version": "0.48.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,11 +18,11 @@
18
18
  "dist"
19
19
  ],
20
20
  "dependencies": {
21
- "@glissade/core": "0.47.0",
22
- "@glissade/scene": "0.47.0"
21
+ "@glissade/core": "0.48.0-pre.1",
22
+ "@glissade/scene": "0.48.0-pre.1"
23
23
  },
24
24
  "devDependencies": {
25
- "@glissade/backend-skia": "0.47.0"
25
+ "@glissade/backend-skia": "0.48.0-pre.1"
26
26
  },
27
27
  "repository": {
28
28
  "type": "git",