@glissade/lottie 0.45.0 → 0.46.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 +73 -2
- package/dist/index.js +259 -12
- package/package.json +4 -4
package/dist/index.d.ts
CHANGED
|
@@ -35,7 +35,19 @@ interface ImageSpec extends BaseSpec {
|
|
|
35
35
|
width: number;
|
|
36
36
|
height: number;
|
|
37
37
|
}
|
|
38
|
-
|
|
38
|
+
interface TextSpec extends BaseSpec {
|
|
39
|
+
kind: 'text';
|
|
40
|
+
text: string;
|
|
41
|
+
fill: string;
|
|
42
|
+
fontSize: number;
|
|
43
|
+
fontFamily: string;
|
|
44
|
+
fontWeight?: number;
|
|
45
|
+
fontStyle?: 'normal' | 'italic';
|
|
46
|
+
align?: 'left' | 'center' | 'right';
|
|
47
|
+
letterSpacing?: number;
|
|
48
|
+
lineHeight?: number;
|
|
49
|
+
}
|
|
50
|
+
type NodeSpec = GroupSpec | PathSpec | RectSpec | ImageSpec | TextSpec;
|
|
39
51
|
interface LottieImportResult {
|
|
40
52
|
size: {
|
|
41
53
|
w: number;
|
|
@@ -125,6 +137,59 @@ interface LottieTransform {
|
|
|
125
137
|
sk?: LottieProp;
|
|
126
138
|
sa?: LottieProp;
|
|
127
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* A text document (`t.d.k[n].s`) — the paint + font state of a ty:5 text layer
|
|
142
|
+
* at one keyframe. Modern bodymovin: `fc` is a 0–1 rgb(a) array, `j` is the
|
|
143
|
+
* justification (0 left, 1 right, 2 center — the Lottie/bodymovin convention).
|
|
144
|
+
* Optional fields (`tr`/`lh`) are OMITTED when at their glissade default so the
|
|
145
|
+
* emitted JSON stays minimal + deterministic (mirrors fontSpec()'s omissions).
|
|
146
|
+
*/
|
|
147
|
+
interface LottieTextDocument {
|
|
148
|
+
/** the text string */
|
|
149
|
+
t: string;
|
|
150
|
+
/** font name — references a `fonts.list[n].fName` */
|
|
151
|
+
f: string;
|
|
152
|
+
/** font size (px) */
|
|
153
|
+
s: number;
|
|
154
|
+
/** fill color, 0–1 `[r,g,b]` or `[r,g,b,a]` */
|
|
155
|
+
fc: number[];
|
|
156
|
+
/** justification: 0 = left, 1 = right, 2 = center */
|
|
157
|
+
j: number;
|
|
158
|
+
/** tracking / letter-spacing (px) — omitted when unset */
|
|
159
|
+
tr?: number;
|
|
160
|
+
/** line height (px) — omitted when the glissade lineHeight is the 1.25 default */
|
|
161
|
+
lh?: number;
|
|
162
|
+
/** baseline shift — read-through only (never emitted) */
|
|
163
|
+
ls?: number;
|
|
164
|
+
/** wrap box size `[w,h]` — read-through only (never emitted) */
|
|
165
|
+
sz?: number[];
|
|
166
|
+
/** wrap box position `[x,y]` — read-through only */
|
|
167
|
+
ps?: number[];
|
|
168
|
+
}
|
|
169
|
+
/** One text-document keyframe: the document `s` applied from frame `t` (hold). */
|
|
170
|
+
interface LottieTextDocKeyframe {
|
|
171
|
+
t: number;
|
|
172
|
+
s: LottieTextDocument;
|
|
173
|
+
}
|
|
174
|
+
/** ty:5 text data: keyframed documents (`d.k`) + animators (`a`, always empty here). */
|
|
175
|
+
interface LottieTextData {
|
|
176
|
+
d: {
|
|
177
|
+
k: LottieTextDocKeyframe[];
|
|
178
|
+
};
|
|
179
|
+
a?: unknown[];
|
|
180
|
+
m?: unknown;
|
|
181
|
+
p?: unknown;
|
|
182
|
+
}
|
|
183
|
+
/** A `fonts.list[n]` entry — a font REFERENCE (never embedded; the player supplies it). */
|
|
184
|
+
interface LottieFont {
|
|
185
|
+
fName: string;
|
|
186
|
+
fFamily: string;
|
|
187
|
+
fStyle: string;
|
|
188
|
+
fWeight?: string;
|
|
189
|
+
fPath?: string;
|
|
190
|
+
origin?: number;
|
|
191
|
+
ascent?: number;
|
|
192
|
+
}
|
|
128
193
|
interface LottieLayer {
|
|
129
194
|
ty: number;
|
|
130
195
|
nm?: string;
|
|
@@ -141,6 +206,8 @@ interface LottieLayer {
|
|
|
141
206
|
ao?: number;
|
|
142
207
|
/** shape layer */
|
|
143
208
|
shapes?: LottieShapeItem[];
|
|
209
|
+
/** text layer (ty:5) */
|
|
210
|
+
t?: LottieTextData;
|
|
144
211
|
/** solid */
|
|
145
212
|
sw?: number;
|
|
146
213
|
sh?: number;
|
|
@@ -181,6 +248,10 @@ interface LottieDocument {
|
|
|
181
248
|
ddd?: number;
|
|
182
249
|
layers: LottieLayer[];
|
|
183
250
|
assets?: LottieAsset[];
|
|
251
|
+
/** Font references (ty:5 layers name into `fonts.list[n].fName`). */
|
|
252
|
+
fonts?: {
|
|
253
|
+
list: LottieFont[];
|
|
254
|
+
};
|
|
184
255
|
}
|
|
185
256
|
//#endregion
|
|
186
257
|
//#region src/pathvalue.d.ts
|
|
@@ -234,4 +305,4 @@ interface ImportOptions {
|
|
|
234
305
|
}
|
|
235
306
|
declare function importLottie(json: unknown, opts?: ImportOptions): LottieImportResult;
|
|
236
307
|
//#endregion
|
|
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 };
|
|
308
|
+
export { type CodegenOptions, type ExportOptions, type GroupSpec, type ImageSpec, ImportOptions, KAPPA, type LottieDocument, LottieImportError, type LottieImportResult, type NodeSpec, type PathSpec, type RectSpec, type TextSpec, buildNode, buildNodes, colorPropIsBytes, ellipseContour, exportLottie, generateSceneModule, importLottie, lottieColor, mergeContours, rectContour, reverseContour, shToContour };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Circle, Group, ImageNode, Path, Rect, createScene } from "@glissade/scene";
|
|
1
|
+
import { Circle, Group, ImageNode, Path, Rect, Text, createScene } from "@glissade/scene";
|
|
2
2
|
import { cubicBezier, formatColor, parseColor, sampleTrack, track } from "@glissade/core";
|
|
3
3
|
import "@glissade/core/expr";
|
|
4
4
|
//#region src/spec.ts
|
|
@@ -250,7 +250,8 @@ const SUPPORTED_LAYER_TYPES = new Set([
|
|
|
250
250
|
1,
|
|
251
251
|
2,
|
|
252
252
|
3,
|
|
253
|
-
4
|
|
253
|
+
4,
|
|
254
|
+
5
|
|
254
255
|
]);
|
|
255
256
|
function reject(ctx, errorClass, detail) {
|
|
256
257
|
ctx.problems.push(`[${errorClass}] ${detail}`);
|
|
@@ -369,8 +370,23 @@ function auditLayer(ctx, layer, index, doc) {
|
|
|
369
370
|
const asset = (doc.assets ?? []).find((a) => a.id === layer.refId);
|
|
370
371
|
if (!asset || typeof asset.p !== "string") reject(ctx, "invalid-asset", `image ${where} references missing asset '${layer.refId ?? ""}'`);
|
|
371
372
|
}
|
|
373
|
+
if (layer.ty === 5) auditTextLayer(ctx, layer, where);
|
|
372
374
|
if (layer.shapes) auditShapeItems(ctx, layer.shapes, where);
|
|
373
375
|
}
|
|
376
|
+
/**
|
|
377
|
+
* Text layers (ty:5) import a static-or-doc-keyframed document; the range-selector
|
|
378
|
+
* ANIMATOR list (`t.a`) has no glissade analogue, so a non-empty one is rejected —
|
|
379
|
+
* loud, not silent (§1). A malformed / missing document is an invalid document.
|
|
380
|
+
*/
|
|
381
|
+
function auditTextLayer(ctx, layer, where) {
|
|
382
|
+
const t = layer.t;
|
|
383
|
+
const keys = t?.d?.k;
|
|
384
|
+
if (!Array.isArray(keys) || keys.length === 0 || typeof keys[0]?.s?.t !== "string") {
|
|
385
|
+
reject(ctx, "invalid-text", `${where}: text layer has no readable document (t.d.k[0].s.t)`);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
if (Array.isArray(t?.a) && t.a.length > 0) reject(ctx, "unsupported-text-animator", `${where}: text range-selector animators (t.a) are not supported`);
|
|
389
|
+
}
|
|
374
390
|
/** Throws LottieImportError listing EVERY rejection; returns collected warnings. */
|
|
375
391
|
function auditDocument(doc, allowDegraded) {
|
|
376
392
|
const ctx = {
|
|
@@ -422,6 +438,18 @@ function buildNode(spec) {
|
|
|
422
438
|
width: spec.width,
|
|
423
439
|
height: spec.height
|
|
424
440
|
});
|
|
441
|
+
case "text": return new Text({
|
|
442
|
+
...base,
|
|
443
|
+
text: spec.text,
|
|
444
|
+
fill: spec.fill,
|
|
445
|
+
fontSize: spec.fontSize,
|
|
446
|
+
fontFamily: spec.fontFamily,
|
|
447
|
+
...spec.fontWeight !== void 0 ? { fontWeight: spec.fontWeight } : {},
|
|
448
|
+
...spec.fontStyle !== void 0 ? { fontStyle: spec.fontStyle } : {},
|
|
449
|
+
...spec.align !== void 0 ? { align: spec.align } : {},
|
|
450
|
+
...spec.letterSpacing !== void 0 ? { letterSpacing: spec.letterSpacing } : {},
|
|
451
|
+
...spec.lineHeight !== void 0 ? { lineHeight: spec.lineHeight } : {}
|
|
452
|
+
});
|
|
425
453
|
}
|
|
426
454
|
}
|
|
427
455
|
function buildNodes(specs) {
|
|
@@ -941,6 +969,80 @@ function denormItems(ctx, items, idBase, tm, where) {
|
|
|
941
969
|
}
|
|
942
970
|
return out;
|
|
943
971
|
}
|
|
972
|
+
/** OTF/bodymovin weight-class names → numeric weight, the inverse of the exporter's map. */
|
|
973
|
+
const WEIGHT_BY_NAME = {
|
|
974
|
+
Thin: 100,
|
|
975
|
+
ExtraLight: 200,
|
|
976
|
+
Light: 300,
|
|
977
|
+
Regular: 400,
|
|
978
|
+
Medium: 500,
|
|
979
|
+
SemiBold: 600,
|
|
980
|
+
Bold: 700,
|
|
981
|
+
ExtraBold: 800,
|
|
982
|
+
Black: 900
|
|
983
|
+
};
|
|
984
|
+
/** Numeric weight from a font ref: prefer `fWeight`, else read a class name out of `fStyle`. */
|
|
985
|
+
function fontWeightFrom(font) {
|
|
986
|
+
if (font === void 0) return void 0;
|
|
987
|
+
if (font.fWeight !== void 0) {
|
|
988
|
+
const n = Number(font.fWeight);
|
|
989
|
+
if (Number.isFinite(n)) return n;
|
|
990
|
+
}
|
|
991
|
+
for (const word of font.fStyle.split(" ")) if (WEIGHT_BY_NAME[word] !== void 0) return WEIGHT_BY_NAME[word];
|
|
992
|
+
}
|
|
993
|
+
/** Justification (0 left / 1 right / 2 center) → glissade align. */
|
|
994
|
+
const justificationToAlign = (j) => j === 1 ? "right" : j === 2 ? "center" : "left";
|
|
995
|
+
/**
|
|
996
|
+
* A varying text-document field → a HOLD track (the document switches discretely
|
|
997
|
+
* between keyframes). Consecutive-equal values collapse; a field constant across
|
|
998
|
+
* the whole stream produces no track (the static base value on the spec suffices).
|
|
999
|
+
*/
|
|
1000
|
+
function pushDocTrack(ctx, target, type, dks, tm, extract) {
|
|
1001
|
+
const keys = [];
|
|
1002
|
+
let prev;
|
|
1003
|
+
for (const dk of dks) {
|
|
1004
|
+
const value = extract(dk.s);
|
|
1005
|
+
const sig = JSON.stringify(value);
|
|
1006
|
+
if (sig === prev) continue;
|
|
1007
|
+
const k = {
|
|
1008
|
+
t: toSeconds(tm, dk.t),
|
|
1009
|
+
value
|
|
1010
|
+
};
|
|
1011
|
+
if (keys.length > 0) k.interp = "hold";
|
|
1012
|
+
keys.push(k);
|
|
1013
|
+
prev = sig;
|
|
1014
|
+
}
|
|
1015
|
+
if (keys.length <= 1) return;
|
|
1016
|
+
ctx.tracks.push(track(target, type, enforceMonotonic(keys)));
|
|
1017
|
+
}
|
|
1018
|
+
/** ty:5 layer → a Text spec (+ hold tracks for any animated text/fill/fontSize). */
|
|
1019
|
+
function convertTextLayer(ctx, layer, base, tm) {
|
|
1020
|
+
const dks = layer.t.d.k;
|
|
1021
|
+
const first = dks[0].s;
|
|
1022
|
+
const font = (ctx.doc.fonts?.list ?? []).find((f) => f.fName === first.f);
|
|
1023
|
+
const spec = {
|
|
1024
|
+
kind: "text",
|
|
1025
|
+
id: uid(ctx, `${base}__text`),
|
|
1026
|
+
text: first.t,
|
|
1027
|
+
fill: lottieColor(first.fc, colorPropIsBytes([first.fc])),
|
|
1028
|
+
fontSize: first.s,
|
|
1029
|
+
fontFamily: font?.fFamily ?? first.f
|
|
1030
|
+
};
|
|
1031
|
+
const weight = fontWeightFrom(font);
|
|
1032
|
+
if (weight !== void 0) spec.fontWeight = weight;
|
|
1033
|
+
if (font !== void 0 && /italic/i.test(font.fStyle)) spec.fontStyle = "italic";
|
|
1034
|
+
const align = justificationToAlign(first.j);
|
|
1035
|
+
if (align !== "left") spec.align = align;
|
|
1036
|
+
if (first.tr !== void 0) spec.letterSpacing = first.tr;
|
|
1037
|
+
if (first.lh !== void 0 && first.s > 0) spec.lineHeight = first.lh / first.s;
|
|
1038
|
+
if (dks.length > 1) {
|
|
1039
|
+
pushDocTrack(ctx, `${spec.id}/text`, "string", dks, tm, (s) => s.t);
|
|
1040
|
+
pushDocTrack(ctx, `${spec.id}/fontSize`, "number", dks, tm, (s) => s.s);
|
|
1041
|
+
const bytes = colorPropIsBytes(dks.map((k) => k.s.fc));
|
|
1042
|
+
pushDocTrack(ctx, `${spec.id}/fill`, "color", dks, tm, (s) => lottieColor(s.fc, bytes));
|
|
1043
|
+
}
|
|
1044
|
+
return spec;
|
|
1045
|
+
}
|
|
944
1046
|
function visibilityWrapper(ctx, base, layer, content, ind) {
|
|
945
1047
|
const { doc } = ctx;
|
|
946
1048
|
const docDur = (doc.op - doc.ip) / doc.fr;
|
|
@@ -1026,7 +1128,7 @@ function convertLayer(ctx, layer, index) {
|
|
|
1026
1128
|
position: [w / 2, h / 2]
|
|
1027
1129
|
};
|
|
1028
1130
|
contentChildren.push(image);
|
|
1029
|
-
}
|
|
1131
|
+
} else if (layer.ty === 5 && layer.t) contentChildren.push(convertTextLayer(ctx, layer, base, tm));
|
|
1030
1132
|
if (layer.ty !== 3 && layer.hd !== true) {
|
|
1031
1133
|
const content = {
|
|
1032
1134
|
kind: "group",
|
|
@@ -1084,7 +1186,8 @@ const CTOR = {
|
|
|
1084
1186
|
group: "Group",
|
|
1085
1187
|
path: "Path",
|
|
1086
1188
|
rect: "Rect",
|
|
1087
|
-
image: "ImageNode"
|
|
1189
|
+
image: "ImageNode",
|
|
1190
|
+
text: "Text"
|
|
1088
1191
|
};
|
|
1089
1192
|
function lit(value, indent) {
|
|
1090
1193
|
return JSON.stringify(value, null, 2).split("\n").join(`\n${indent}`);
|
|
@@ -1117,6 +1220,14 @@ function emitNode(spec, indent) {
|
|
|
1117
1220
|
case "image":
|
|
1118
1221
|
props.push(`assetId: ${JSON.stringify(spec.assetId)}`, `width: ${spec.width}`, `height: ${spec.height}`);
|
|
1119
1222
|
break;
|
|
1223
|
+
case "text":
|
|
1224
|
+
props.push(`text: ${JSON.stringify(spec.text)}`, `fill: ${JSON.stringify(spec.fill)}`, `fontSize: ${spec.fontSize}`, `fontFamily: ${JSON.stringify(spec.fontFamily)}`);
|
|
1225
|
+
if (spec.fontWeight !== void 0) props.push(`fontWeight: ${spec.fontWeight}`);
|
|
1226
|
+
if (spec.fontStyle !== void 0) props.push(`fontStyle: ${JSON.stringify(spec.fontStyle)}`);
|
|
1227
|
+
if (spec.align !== void 0) props.push(`align: ${JSON.stringify(spec.align)}`);
|
|
1228
|
+
if (spec.letterSpacing !== void 0) props.push(`letterSpacing: ${spec.letterSpacing}`);
|
|
1229
|
+
if (spec.lineHeight !== void 0) props.push(`lineHeight: ${spec.lineHeight}`);
|
|
1230
|
+
break;
|
|
1120
1231
|
}
|
|
1121
1232
|
return `new ${CTOR[spec.kind]}({\n${props.map((p) => `${inner}${p},`).join("\n")}\n${indent}})`;
|
|
1122
1233
|
}
|
|
@@ -1386,10 +1497,15 @@ function decimateLinearKeys(keys, relEps = .002) {
|
|
|
1386
1497
|
* silent). IN: Group hierarchy, Rect/Circle/Path with a SOLID fill (+ optional
|
|
1387
1498
|
* stroke), transform channels (position / position.x/.y split, opacity, scale,
|
|
1388
1499
|
* rotation → identity degrees), animated `fill` color, animated `d` path
|
|
1389
|
-
* (constant topology)
|
|
1390
|
-
*
|
|
1391
|
-
*
|
|
1392
|
-
*
|
|
1500
|
+
* (constant topology), and TEXT (ty:5): a Text node → a text layer with a font
|
|
1501
|
+
* reference (fonts.list) + a text-document keyframe stream (static = one doc;
|
|
1502
|
+
* animated text/fill/fontSize → doc keyframes sampled on the frame grid, held).
|
|
1503
|
+
* 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
|
|
1506
|
+
* (`fontAxes`/`fontVariationSettings` — no Lottie doc field), `box` valign
|
|
1507
|
+
* (baseline-approximated) and wrap `width` (the player self-reflows), TokenHighlight.
|
|
1508
|
+
* Animated primitive geometry (width/radius tracks) is SAMPLED, not channel-mapped.
|
|
1393
1509
|
*/
|
|
1394
1510
|
const EMPTY_TRACKS = /* @__PURE__ */ new Map();
|
|
1395
1511
|
/** Convert a SceneModule to a Lottie document. Pure over (scene, timeline). */
|
|
@@ -1419,9 +1535,11 @@ function exportLottie(mod, opts) {
|
|
|
1419
1535
|
op,
|
|
1420
1536
|
warn,
|
|
1421
1537
|
layers: [],
|
|
1422
|
-
ind: 0
|
|
1538
|
+
ind: 0,
|
|
1539
|
+
fonts: /* @__PURE__ */ new Map()
|
|
1423
1540
|
};
|
|
1424
1541
|
walkChildren(ctx, scene.root.children, void 0, byNode);
|
|
1542
|
+
const fonts = [...ctx.fonts.values()];
|
|
1425
1543
|
return {
|
|
1426
1544
|
v: BODYMOVIN_VERSION,
|
|
1427
1545
|
fr,
|
|
@@ -1430,7 +1548,8 @@ function exportLottie(mod, opts) {
|
|
|
1430
1548
|
w: opts.width,
|
|
1431
1549
|
h: opts.height,
|
|
1432
1550
|
nm: "glissade export",
|
|
1433
|
-
layers: ctx.layers
|
|
1551
|
+
layers: ctx.layers,
|
|
1552
|
+
...fonts.length > 0 ? { fonts: { list: fonts } } : {}
|
|
1434
1553
|
};
|
|
1435
1554
|
}
|
|
1436
1555
|
/**
|
|
@@ -1466,12 +1585,12 @@ function walkChildren(ctx, children, parentInd, byNode) {
|
|
|
1466
1585
|
const node = children[i];
|
|
1467
1586
|
const kind = classify(node);
|
|
1468
1587
|
if (kind === "drop") {
|
|
1469
|
-
ctx.warn(`${describe(node)} is not exportable (MVP: Group / Rect / Circle / Path) — dropped`);
|
|
1588
|
+
ctx.warn(`${describe(node)} is not exportable (MVP: Group / Rect / Circle / Path / Text) — dropped`);
|
|
1470
1589
|
continue;
|
|
1471
1590
|
}
|
|
1472
1591
|
const myInd = ++ctx.ind;
|
|
1473
1592
|
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));
|
|
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));
|
|
1475
1594
|
if (node instanceof Group) walkChildren(ctx, node.children, myInd, byNode);
|
|
1476
1595
|
}
|
|
1477
1596
|
}
|
|
@@ -1479,6 +1598,7 @@ function classify(node) {
|
|
|
1479
1598
|
if (node instanceof Rect) return "rect";
|
|
1480
1599
|
if (node instanceof Circle) return "circle";
|
|
1481
1600
|
if (node instanceof Path) return "path";
|
|
1601
|
+
if (node instanceof Text) return "text";
|
|
1482
1602
|
if (node instanceof Group) return "group";
|
|
1483
1603
|
return "drop";
|
|
1484
1604
|
}
|
|
@@ -1625,6 +1745,133 @@ function buildShapeLayer(ctx, node, kind, ind, parentInd, tracks) {
|
|
|
1625
1745
|
...parentInd !== void 0 ? { parent: parentInd } : {}
|
|
1626
1746
|
};
|
|
1627
1747
|
}
|
|
1748
|
+
/** Human weight-class names (400 = Regular), the OTF/bodymovin `fStyle` convention. */
|
|
1749
|
+
const WEIGHT_NAMES = {
|
|
1750
|
+
100: "Thin",
|
|
1751
|
+
200: "ExtraLight",
|
|
1752
|
+
300: "Light",
|
|
1753
|
+
400: "Regular",
|
|
1754
|
+
500: "Medium",
|
|
1755
|
+
600: "SemiBold",
|
|
1756
|
+
700: "Bold",
|
|
1757
|
+
800: "ExtraBold",
|
|
1758
|
+
900: "Black"
|
|
1759
|
+
};
|
|
1760
|
+
/** `fStyle` from weight + italic — e.g. 700 + italic → 'Bold Italic', 400 → 'Regular'. */
|
|
1761
|
+
function fontStyleName(weight, italic) {
|
|
1762
|
+
const name = WEIGHT_NAMES[weight] ?? String(weight);
|
|
1763
|
+
if (name === "Regular") return italic ? "Italic" : "Regular";
|
|
1764
|
+
return italic ? `${name} Italic` : name;
|
|
1765
|
+
}
|
|
1766
|
+
/**
|
|
1767
|
+
* Register (family, weight, style) once, returning the stable `fName` the text
|
|
1768
|
+
* document references. De-dupe is keyed on the derived fName, so two Text nodes
|
|
1769
|
+
* sharing a face share one `fonts.list` entry (pure + insertion-ordered). Fonts
|
|
1770
|
+
* are referenced by name only — never embedded; the player supplies the face
|
|
1771
|
+
* (the §3.6 registry papercut).
|
|
1772
|
+
*/
|
|
1773
|
+
function registerFont(ctx, node) {
|
|
1774
|
+
const italic = node.fontStyle === "italic";
|
|
1775
|
+
const fStyle = fontStyleName(node.fontWeight, italic);
|
|
1776
|
+
const fName = `${node.fontFamily}-${fStyle.replace(/ /g, "")}`;
|
|
1777
|
+
if (!ctx.fonts.has(fName)) ctx.fonts.set(fName, {
|
|
1778
|
+
fName,
|
|
1779
|
+
fFamily: node.fontFamily,
|
|
1780
|
+
fStyle,
|
|
1781
|
+
fWeight: String(node.fontWeight)
|
|
1782
|
+
});
|
|
1783
|
+
return fName;
|
|
1784
|
+
}
|
|
1785
|
+
/** Justification: glissade align → the Lottie/bodymovin `j` (0 left, 1 right, 2 center). */
|
|
1786
|
+
function alignToJustification(align) {
|
|
1787
|
+
return align === "left" ? 0 : align === "right" ? 1 : 2;
|
|
1788
|
+
}
|
|
1789
|
+
/** The text document at time `t`, sampling the animatable text/fill/fontSize props. */
|
|
1790
|
+
function textDocAt(node, fName, tracks, t) {
|
|
1791
|
+
const text = sampleStr(tracks, "text", node.text(), t);
|
|
1792
|
+
const fill = sampleColor(tracks, "fill", node.fill(), t);
|
|
1793
|
+
const size = sampleNum(tracks, "fontSize", node.fontSize(), t);
|
|
1794
|
+
return {
|
|
1795
|
+
t: text,
|
|
1796
|
+
f: fName,
|
|
1797
|
+
s: size,
|
|
1798
|
+
fc: colorToLottie(fill),
|
|
1799
|
+
j: alignToJustification(node.align),
|
|
1800
|
+
...node.letterSpacing !== void 0 ? { tr: node.letterSpacing } : {},
|
|
1801
|
+
...node.lineHeight !== 1.25 ? { lh: size * node.lineHeight } : {}
|
|
1802
|
+
};
|
|
1803
|
+
}
|
|
1804
|
+
const sampleStr = (tracks, prop, staticVal, t) => {
|
|
1805
|
+
const tr = tracks.get(prop);
|
|
1806
|
+
return tr ? sampleTrack(tr, t) : staticVal;
|
|
1807
|
+
};
|
|
1808
|
+
const sampleNum = (tracks, prop, staticVal, t) => {
|
|
1809
|
+
const tr = tracks.get(prop);
|
|
1810
|
+
return tr ? sampleTrack(tr, t) : staticVal;
|
|
1811
|
+
};
|
|
1812
|
+
const sampleColor = (tracks, prop, staticVal, t) => {
|
|
1813
|
+
const tr = tracks.get(prop);
|
|
1814
|
+
return tr && tr.type === "color" ? sampleTrack(tr, t) : staticVal;
|
|
1815
|
+
};
|
|
1816
|
+
/**
|
|
1817
|
+
* The text-document keyframe stream. STATIC (no text/fill/fontSize track) = one
|
|
1818
|
+
* document at t=0. ANIMATED = one document per frame across the animated span,
|
|
1819
|
+
* SAMPLED and held (a Lottie text document switches discretely — smooth fill/size
|
|
1820
|
+
* animation degrades to a per-frame step, the honest MVP limit). Consecutive
|
|
1821
|
+
* identical documents collapse to their first frame so a constant prop stays lean.
|
|
1822
|
+
*/
|
|
1823
|
+
function buildTextDocKeyframes(ctx, node, fName, tracks) {
|
|
1824
|
+
const docProps = [
|
|
1825
|
+
"text",
|
|
1826
|
+
"fill",
|
|
1827
|
+
"fontSize"
|
|
1828
|
+
];
|
|
1829
|
+
if (!docProps.some((p) => tracks.has(p))) return [{
|
|
1830
|
+
t: ctx.ip,
|
|
1831
|
+
s: textDocAt(node, fName, tracks, ctx.ip / ctx.fr)
|
|
1832
|
+
}];
|
|
1833
|
+
ctx.warn(`${describe(node)}: animated text/fill/fontSize is sampled at ${ctx.fr} fps into stepped text documents (not smoothly interpolated)`);
|
|
1834
|
+
const [f0, f1] = frameSpan(ctx, docProps.map((p) => tracks.get(p)));
|
|
1835
|
+
const keys = [];
|
|
1836
|
+
let prev;
|
|
1837
|
+
for (let f = f0; f <= f1; f++) {
|
|
1838
|
+
const s = textDocAt(node, fName, tracks, f / ctx.fr);
|
|
1839
|
+
const sig = JSON.stringify(s);
|
|
1840
|
+
if (sig === prev) continue;
|
|
1841
|
+
keys.push({
|
|
1842
|
+
t: f,
|
|
1843
|
+
s
|
|
1844
|
+
});
|
|
1845
|
+
prev = sig;
|
|
1846
|
+
}
|
|
1847
|
+
return keys;
|
|
1848
|
+
}
|
|
1849
|
+
/** Warn (never silent) on the text features this MVP cannot represent, then drop them. */
|
|
1850
|
+
function warnTextUnsupported(ctx, node, tracks) {
|
|
1851
|
+
if (tracks.has("reveal") || Number.isFinite(node.reveal())) ctx.warn(`${describe(node)}: typewriter 'reveal' is not exported (Lottie range selectors are a later phase) — dropped, full text shown`);
|
|
1852
|
+
if (tracks.has("revealFraction") || !Number.isNaN(node.revealFraction())) ctx.warn(`${describe(node)}: 'revealFraction' is not exported — dropped, full text shown`);
|
|
1853
|
+
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`);
|
|
1854
|
+
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
|
+
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
|
+
}
|
|
1857
|
+
function buildTextLayer(ctx, node, ind, parentInd, tracks) {
|
|
1858
|
+
const fName = registerFont(ctx, node);
|
|
1859
|
+
warnTextUnsupported(ctx, node, tracks);
|
|
1860
|
+
const t = {
|
|
1861
|
+
d: { k: buildTextDocKeyframes(ctx, node, fName, tracks) },
|
|
1862
|
+
a: []
|
|
1863
|
+
};
|
|
1864
|
+
return {
|
|
1865
|
+
ty: 5,
|
|
1866
|
+
nm: node.id ?? `text${ind}`,
|
|
1867
|
+
ind,
|
|
1868
|
+
ip: ctx.ip,
|
|
1869
|
+
op: ctx.op,
|
|
1870
|
+
ks: buildTransform(ctx, node, tracks),
|
|
1871
|
+
t,
|
|
1872
|
+
...parentInd !== void 0 ? { parent: parentInd } : {}
|
|
1873
|
+
};
|
|
1874
|
+
}
|
|
1628
1875
|
function buildGeometry(ctx, node, kind, tracks) {
|
|
1629
1876
|
if (kind === "path") return buildPathGeometry(ctx, node, tracks);
|
|
1630
1877
|
const paramNames = kind === "rect" ? [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glissade/lottie",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.46.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.
|
|
22
|
-
"@glissade/scene": "0.
|
|
21
|
+
"@glissade/core": "0.46.0-pre.0",
|
|
22
|
+
"@glissade/scene": "0.46.0-pre.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@glissade/backend-skia": "0.
|
|
25
|
+
"@glissade/backend-skia": "0.46.0-pre.0"
|
|
26
26
|
},
|
|
27
27
|
"repository": {
|
|
28
28
|
"type": "git",
|