@glissade/lottie 0.51.0 → 0.52.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, SceneModule } from "@glissade/scene";
2
+ import { Node, SceneModule, TextMeasurer } from "@glissade/scene";
3
3
  import { Paint, PathContour, PathValue, Timeline, Vec2 } from "@glissade/core";
4
4
 
5
5
  //#region src/spec.d.ts
@@ -310,6 +310,16 @@ interface ExportOptions {
310
310
  fps?: number;
311
311
  /** Sink for scope-out / degrade warnings; default `console.warn`. */
312
312
  onWarn?: (message: string) => void;
313
+ /**
314
+ * Real text measurer (a backend's — e.g. the Skia measurer). When present, a
315
+ * width-wrapped Text node has its wrapped lines BAKED into the Lottie text
316
+ * document `t` (joined by '\n') so the round-trip reproduces glissade's line
317
+ * breaks — the importer copies `t` verbatim and drops the wrap `width`, so
318
+ * without the bake wrapped text collapses onto one line. Absent, the raw
319
+ * string passes through unchanged (the estimating measurer's wrap points
320
+ * wouldn't match a faithful render, so we don't bake with it).
321
+ */
322
+ measurer?: TextMeasurer;
313
323
  }
314
324
  /** Convert a SceneModule to a Lottie document. Pure over (scene, timeline). */
315
325
  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, createScene } from "@glissade/scene";
1
+ import { Circle, Group, ImageNode, Path, Rect, Text, breakLines, createScene } 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
@@ -1705,6 +1705,7 @@ const IDENTITY_OPACITY = {
1705
1705
  /** Convert a SceneModule to a Lottie document. Pure over (scene, timeline). */
1706
1706
  function exportLottie(mod, opts) {
1707
1707
  const scene = mod.createScene();
1708
+ if (opts.measurer) scene.setTextMeasurer(opts.measurer);
1708
1709
  const fr = opts.fps ?? mod.timeline.fps ?? 60;
1709
1710
  const warn = opts.onWarn ?? ((m) => console.warn(`gs export: ${m}`));
1710
1711
  const compiled = compileTimeline(mod.timeline);
@@ -1731,7 +1732,8 @@ function exportLottie(mod, opts) {
1731
1732
  warn,
1732
1733
  layers: [],
1733
1734
  ind: 0,
1734
- fonts: /* @__PURE__ */ new Map()
1735
+ fonts: /* @__PURE__ */ new Map(),
1736
+ measurer: opts.measurer
1735
1737
  };
1736
1738
  walkChildren(ctx, scene.root.children, void 0, byNode, IDENTITY_OPACITY);
1737
1739
  const fonts = [...ctx.fonts.values()];
@@ -2056,13 +2058,30 @@ function registerFont(ctx, node) {
2056
2058
  function alignToJustification(align) {
2057
2059
  return align === "left" ? 0 : align === "right" ? 1 : 2;
2058
2060
  }
2061
+ /**
2062
+ * The FontSpec at `t` for the wrap measurement, mirroring `Text.fontSpec()` but
2063
+ * with the SAMPLED size so animated `fontSize` re-wraps per frame. Weight, style,
2064
+ * static variable-font axes, and letter-spacing all feed `measureText` (they move
2065
+ * wrap points), so they must be present to match the reference render's breaks.
2066
+ */
2067
+ function wrapFontSpec(node, size) {
2068
+ return {
2069
+ family: node.fontFamily,
2070
+ size,
2071
+ weight: node.fontWeight,
2072
+ ...node.fontStyle === "italic" ? { style: "italic" } : {},
2073
+ ...node.fontVariationSettings !== void 0 ? { fontVariationSettings: node.fontVariationSettings } : {},
2074
+ ...node.letterSpacing !== void 0 ? { letterSpacing: node.letterSpacing } : {}
2075
+ };
2076
+ }
2059
2077
  /** The text document at time `t`, sampling the animatable text/fill/fontSize props. */
2060
- function textDocAt(node, fName, tracks, t) {
2061
- const text = sampleStr(tracks, "text", node.text(), t);
2078
+ function textDocAt(ctx, node, fName, tracks, t) {
2079
+ const rawText = sampleStr(tracks, "text", node.text(), t);
2062
2080
  const fill = sampleColor(tracks, "fill", node.fill(), t);
2063
2081
  const size = sampleNum(tracks, "fontSize", node.fontSize(), t);
2082
+ const width = sampleNum(tracks, "width", node.width(), t);
2064
2083
  return {
2065
- t: text,
2084
+ t: width > 0 && ctx.measurer !== void 0 ? breakLines(rawText, wrapFontSpec(node, size), width, ctx.measurer).join("\n") : rawText,
2066
2085
  f: fName,
2067
2086
  s: size,
2068
2087
  fc: colorToLottie(fill),
@@ -2096,16 +2115,17 @@ function buildTextDocKeyframes(ctx, node, fName, tracks) {
2096
2115
  "fill",
2097
2116
  "fontSize"
2098
2117
  ];
2099
- if (!docProps.some((p) => tracks.has(p))) return [{
2118
+ const streamProps = ctx.measurer !== void 0 && tracks.has("width") ? [...docProps, "width"] : docProps;
2119
+ if (!streamProps.some((p) => tracks.has(p))) return [{
2100
2120
  t: ctx.ip,
2101
- s: textDocAt(node, fName, tracks, ctx.ip / ctx.fr)
2121
+ s: textDocAt(ctx, node, fName, tracks, ctx.ip / ctx.fr)
2102
2122
  }];
2103
- ctx.warn(`${describe(node)}: animated text/fill/fontSize is sampled at ${ctx.fr} fps into stepped text documents (not smoothly interpolated)`);
2104
- const [f0, f1] = frameSpan(ctx, docProps.map((p) => tracks.get(p)));
2123
+ if (docProps.some((p) => tracks.has(p))) ctx.warn(`${describe(node)}: animated text/fill/fontSize is sampled at ${ctx.fr} fps into stepped text documents (not smoothly interpolated)`);
2124
+ const [f0, f1] = frameSpan(ctx, streamProps.map((p) => tracks.get(p)));
2105
2125
  const keys = [];
2106
2126
  let prev;
2107
2127
  for (let f = f0; f <= f1; f++) {
2108
- const s = textDocAt(node, fName, tracks, f / ctx.fr);
2128
+ const s = textDocAt(ctx, node, fName, tracks, f / ctx.fr);
2109
2129
  const sig = JSON.stringify(s);
2110
2130
  if (sig === prev) continue;
2111
2131
  keys.push({
@@ -2122,7 +2142,7 @@ function warnTextUnsupported(ctx, node, tracks) {
2122
2142
  if (tracks.has("revealFraction") || !Number.isNaN(node.revealFraction())) ctx.warn(`${describe(node)}: 'revealFraction' is not exported — dropped, full text shown`);
2123
2143
  if (tracks.has("fontAxes") || Object.keys(node.fontAxes()).length > 0 || node.fontVariationSettings !== void 0) ctx.warn(`${describe(node)}: variable-font axes (fontAxes/fontVariationSettings) have no Lottie text-document field — dropped`);
2124
2144
  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`);
2125
- 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`);
2145
+ if (ctx.measurer === void 0 && (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`);
2126
2146
  }
2127
2147
  function buildTextLayer(ctx, node, ind, parentInd, tracks, opacity) {
2128
2148
  const fName = registerFont(ctx, node);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glissade/lottie",
3
- "version": "0.51.0",
3
+ "version": "0.52.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.51.0",
22
- "@glissade/scene": "0.51.0"
21
+ "@glissade/core": "0.52.0",
22
+ "@glissade/scene": "0.52.0"
23
23
  },
24
24
  "devDependencies": {
25
- "@glissade/backend-skia": "0.51.0"
25
+ "@glissade/backend-skia": "0.52.0"
26
26
  },
27
27
  "repository": {
28
28
  "type": "git",