@glissade/lottie 0.50.0 → 0.51.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.
- package/dist/index.d.ts +20 -4
- package/dist/index.js +389 -18
- package/package.json +4 -4
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as _glissade_scene0 from "@glissade/scene";
|
|
2
2
|
import { Node, SceneModule } from "@glissade/scene";
|
|
3
|
-
import { PathContour, PathValue, Timeline, Vec2 } from "@glissade/core";
|
|
3
|
+
import { Paint, PathContour, PathValue, Timeline, Vec2 } from "@glissade/core";
|
|
4
4
|
|
|
5
5
|
//#region src/spec.d.ts
|
|
6
6
|
|
|
@@ -19,7 +19,8 @@ interface GroupSpec extends BaseSpec {
|
|
|
19
19
|
interface PathSpec extends BaseSpec {
|
|
20
20
|
kind: 'path';
|
|
21
21
|
data: PathValue;
|
|
22
|
-
|
|
22
|
+
/** A CSS color string, or a linear/radial gradient `Paint` (imported from `gf`). */
|
|
23
|
+
fill?: string | Paint;
|
|
23
24
|
stroke?: string;
|
|
24
25
|
strokeWidth?: number;
|
|
25
26
|
}
|
|
@@ -102,6 +103,16 @@ interface LottieShapePathData {
|
|
|
102
103
|
o: number[][];
|
|
103
104
|
c?: boolean;
|
|
104
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* A gradient color ramp (`gf`/`gs` `.g`): `p` = the number of COLOR stops, `k`
|
|
108
|
+
* the animatable flattened array. Static `k` is `[offset,r,g,b, …]` (p color
|
|
109
|
+
* stops, 0–1 floats) optionally followed by `[offset,a, …]` alpha stops when any
|
|
110
|
+
* stop is translucent; the split point is `p*4`.
|
|
111
|
+
*/
|
|
112
|
+
interface LottieGradient {
|
|
113
|
+
p: number;
|
|
114
|
+
k: LottieProp;
|
|
115
|
+
}
|
|
105
116
|
interface LottieShapeItem {
|
|
106
117
|
/** shape direction: 3 = reversed winding (el/rc). */
|
|
107
118
|
d?: number | {
|
|
@@ -115,11 +126,11 @@ interface LottieShapeItem {
|
|
|
115
126
|
/** sh */
|
|
116
127
|
ks?: LottieProp;
|
|
117
128
|
closed?: boolean;
|
|
118
|
-
/** el / rc / tr */
|
|
129
|
+
/** el / rc / tr — also gf/gs gradient START point (s) + highlight angle (a). */
|
|
119
130
|
p?: LottieProp;
|
|
120
131
|
s?: LottieProp;
|
|
121
132
|
a?: LottieProp;
|
|
122
|
-
/** rc corner radius / fl-st opacity-adjacent fields */
|
|
133
|
+
/** rc corner radius / fl-st opacity-adjacent fields / gf-gs fill rule. */
|
|
123
134
|
r?: LottieProp | number;
|
|
124
135
|
/** fl / st */
|
|
125
136
|
c?: LottieProp;
|
|
@@ -127,6 +138,11 @@ interface LottieShapeItem {
|
|
|
127
138
|
w?: LottieProp;
|
|
128
139
|
/** mm */
|
|
129
140
|
mm?: number;
|
|
141
|
+
/** gf / gs gradient: type (1 linear, 2 radial), END point, highlight length, color ramp. */
|
|
142
|
+
t?: number;
|
|
143
|
+
e?: LottieProp;
|
|
144
|
+
h?: LottieProp;
|
|
145
|
+
g?: LottieGradient;
|
|
130
146
|
}
|
|
131
147
|
interface LottieTransform {
|
|
132
148
|
a?: LottieProp;
|
package/dist/index.js
CHANGED
|
@@ -289,7 +289,6 @@ const UNSUPPORTED_SHAPE_ITEMS = {
|
|
|
289
289
|
rp: "repeater",
|
|
290
290
|
rd: "round corners",
|
|
291
291
|
sr: "polystar",
|
|
292
|
-
gf: "gradient fill",
|
|
293
292
|
gs: "gradient stroke",
|
|
294
293
|
zz: "zig-zag",
|
|
295
294
|
op: "offset path",
|
|
@@ -337,6 +336,20 @@ function auditShapeItems(ctx, items, where) {
|
|
|
337
336
|
checkExpression(ctx, item.o, `${here}.o`);
|
|
338
337
|
checkExpression(ctx, item.w, `${here}.w`);
|
|
339
338
|
break;
|
|
339
|
+
case "gf": {
|
|
340
|
+
if (sawGroup) reject(ctx, "unsupported-shape-structure", `style inheriting into a preceding group at ${here}`);
|
|
341
|
+
if (typeof item.r === "number" && item.r === 2) reject(ctx, "unsupported-fill-rule", `even-odd gradient fill at ${here}`);
|
|
342
|
+
if (item.t !== 1 && item.t !== 2) reject(ctx, "unsupported-gradient", `gradient type ${String(item.t)} (only linear=1 / radial=2) at ${here}`);
|
|
343
|
+
if (!isProp(item.s) || !isProp(item.e)) reject(ctx, "invalid-gradient", `${here}: gradient fill missing start/end point (s/e)`);
|
|
344
|
+
const g = item.g;
|
|
345
|
+
if (!g || typeof g.p !== "number" || g.p < 1 || !isProp(g.k)) reject(ctx, "invalid-gradient", `${here}: gradient fill missing color stops (g.p/g.k)`);
|
|
346
|
+
for (const [name, prop] of [["h", item.h], ["a", item.a]]) if (isProp(prop) && !isKeyframed(prop) && typeof prop.k === "number" && Math.abs(prop.k) > 1e-9) reject(ctx, "unsupported-gradient", `radial highlight (${name}) at ${here}`);
|
|
347
|
+
checkExpression(ctx, item.s, `${here}.s`);
|
|
348
|
+
checkExpression(ctx, item.e, `${here}.e`);
|
|
349
|
+
checkExpression(ctx, item.o, `${here}.o`);
|
|
350
|
+
checkExpression(ctx, g?.k, `${here}.g`);
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
340
353
|
case "mm": {
|
|
341
354
|
const mode = item.mm ?? 1;
|
|
342
355
|
if (mode !== 1) degrade(ctx, "unsupported-shape-modifier", `merge paths mode ${mode} at ${here}`);
|
|
@@ -884,6 +897,112 @@ function mergeGeomSources(ctx, sources, tm, where) {
|
|
|
884
897
|
keys: enforceMonotonic(keys)
|
|
885
898
|
};
|
|
886
899
|
}
|
|
900
|
+
/** Lottie gf color ramp (`g.k`) → glissade ColorStop[]. The first `p` groups of
|
|
901
|
+
* `[offset,r,g,b]` are colors (0–1 floats); trailing `[offset,a]` groups are alpha
|
|
902
|
+
* stops, merged onto each color offset (interpolated across the alpha ramp). */
|
|
903
|
+
function gradientStops(k, p) {
|
|
904
|
+
const alphas = [];
|
|
905
|
+
for (let i = p * 4; i + 1 < k.length; i += 2) alphas.push({
|
|
906
|
+
offset: k[i],
|
|
907
|
+
a: k[i + 1]
|
|
908
|
+
});
|
|
909
|
+
const alphaAt = (offset) => {
|
|
910
|
+
if (alphas.length === 0) return 1;
|
|
911
|
+
let lo = alphas[0];
|
|
912
|
+
if (offset <= lo.offset) return lo.a;
|
|
913
|
+
for (let j = 1; j < alphas.length; j++) {
|
|
914
|
+
const hi = alphas[j];
|
|
915
|
+
if (offset <= hi.offset) {
|
|
916
|
+
const span = hi.offset - lo.offset;
|
|
917
|
+
return span > 0 ? lo.a + (hi.a - lo.a) * (offset - lo.offset) / span : hi.a;
|
|
918
|
+
}
|
|
919
|
+
lo = hi;
|
|
920
|
+
}
|
|
921
|
+
return lo.a;
|
|
922
|
+
};
|
|
923
|
+
const stops = [];
|
|
924
|
+
for (let i = 0; i < p; i++) {
|
|
925
|
+
const o = i * 4;
|
|
926
|
+
const offset = k[o] ?? 0;
|
|
927
|
+
stops.push({
|
|
928
|
+
offset,
|
|
929
|
+
color: formatColor({
|
|
930
|
+
r: (k[o + 1] ?? 0) * 255,
|
|
931
|
+
g: (k[o + 2] ?? 0) * 255,
|
|
932
|
+
b: (k[o + 3] ?? 0) * 255,
|
|
933
|
+
a: alphaAt(offset)
|
|
934
|
+
})
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
return stops;
|
|
938
|
+
}
|
|
939
|
+
/** Reconstruct a static Paint from a gf's start/end points + ramp — the inverse of
|
|
940
|
+
* export.ts's gradientStart/gradientEnd (radial: centre = s, radius = |s→e|). */
|
|
941
|
+
function buildStaticPaint(t, s, e, k, p) {
|
|
942
|
+
const stops = gradientStops(k, p);
|
|
943
|
+
if (t === 2) return {
|
|
944
|
+
kind: "radial",
|
|
945
|
+
stops,
|
|
946
|
+
center: [s[0], s[1]],
|
|
947
|
+
radius: Math.hypot(e[0] - s[0], e[1] - s[1])
|
|
948
|
+
};
|
|
949
|
+
return {
|
|
950
|
+
kind: "linear",
|
|
951
|
+
stops,
|
|
952
|
+
from: [s[0], s[1]],
|
|
953
|
+
to: [e[0], e[1]]
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
/** Set `spec.fill` to a linear/radial Paint (static) or push a `paint` track (animated). */
|
|
957
|
+
function applyGradientFill(ctx, spec, style, tm) {
|
|
958
|
+
const t = style.t;
|
|
959
|
+
const p = style.g?.p ?? 0;
|
|
960
|
+
const sNorm = norms(style.s);
|
|
961
|
+
const eNorm = norms(style.e);
|
|
962
|
+
const gNorm = norms(style.g?.k);
|
|
963
|
+
const staticS = () => vec2Of(style.s?.k ?? [0, 0]);
|
|
964
|
+
const staticE = () => vec2Of(style.e?.k ?? [0, 0]);
|
|
965
|
+
const staticG = () => Array.isArray(style.g?.k.k) ? style.g.k.k : [];
|
|
966
|
+
if (!sNorm && !eNorm && !gNorm) {
|
|
967
|
+
spec.fill = buildStaticPaint(t, staticS(), staticE(), staticG(), p);
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
const channels = [
|
|
971
|
+
sNorm,
|
|
972
|
+
eNorm,
|
|
973
|
+
gNorm
|
|
974
|
+
].filter((n) => n !== void 0);
|
|
975
|
+
const first = channels[0];
|
|
976
|
+
const aligned = channels.every((n) => n.length === first.length && n.every((kk, j) => kk.t === first[j].t));
|
|
977
|
+
let keys;
|
|
978
|
+
if (aligned) {
|
|
979
|
+
keys = convertKeys(gNorm ?? sNorm ?? eNorm, tm, () => ({
|
|
980
|
+
kind: "linear",
|
|
981
|
+
stops: []
|
|
982
|
+
}));
|
|
983
|
+
for (let j = 0; j < keys.length; j++) {
|
|
984
|
+
const s = sNorm ? vec2Of(sNorm[j].value) : staticS();
|
|
985
|
+
const e = eNorm ? vec2Of(eNorm[j].value) : staticE();
|
|
986
|
+
const g = gNorm ? gNorm[j].value : staticG();
|
|
987
|
+
keys[j].value = buildStaticPaint(t, s, e, g, p);
|
|
988
|
+
}
|
|
989
|
+
} else {
|
|
990
|
+
const times = [...new Set(channels.flatMap((n) => n.map((kk) => kk.t)))].sort((a, b) => a - b);
|
|
991
|
+
const at = (norm, ft, fallback, read) => {
|
|
992
|
+
if (!norm) return fallback();
|
|
993
|
+
let v = norm[0].value;
|
|
994
|
+
for (const kk of norm) if (kk.t <= ft) v = kk.value;
|
|
995
|
+
else break;
|
|
996
|
+
return read(v);
|
|
997
|
+
};
|
|
998
|
+
keys = enforceMonotonic(times.map((ft) => ({
|
|
999
|
+
t: toSeconds(tm, ft),
|
|
1000
|
+
value: buildStaticPaint(t, at(sNorm, ft, staticS, vec2Of), at(eNorm, ft, staticE, vec2Of), at(gNorm, ft, staticG, (v) => v), p)
|
|
1001
|
+
})));
|
|
1002
|
+
}
|
|
1003
|
+
spec.fill = keys[0].value;
|
|
1004
|
+
pushTrack(ctx, `${spec.id}/fill`, "paint", keys);
|
|
1005
|
+
}
|
|
887
1006
|
function pathSpecFor(ctx, style, geom, idBase, tm) {
|
|
888
1007
|
const spec = {
|
|
889
1008
|
kind: "path",
|
|
@@ -891,6 +1010,11 @@ function pathSpecFor(ctx, style, geom, idBase, tm) {
|
|
|
891
1010
|
data: geom.kind === "static" ? geom.value : geom.keys[0].value
|
|
892
1011
|
};
|
|
893
1012
|
if (geom.kind === "animated") pushTrack(ctx, `${spec.id}/d`, "path", geom.keys.map((k) => ({ ...k })));
|
|
1013
|
+
if (style.ty === "gf") {
|
|
1014
|
+
applyGradientFill(ctx, spec, style, tm);
|
|
1015
|
+
applyOpacity(ctx, spec, style.o, tm);
|
|
1016
|
+
return spec;
|
|
1017
|
+
}
|
|
894
1018
|
const colorTarget = style.ty === "fl" ? "fill" : "stroke";
|
|
895
1019
|
const cNorm = norms(style.c);
|
|
896
1020
|
if (cNorm) {
|
|
@@ -948,7 +1072,7 @@ function denormItems(ctx, items, idBase, tm, where) {
|
|
|
948
1072
|
source,
|
|
949
1073
|
name: item.nm ?? `geo${geomCounter++}`
|
|
950
1074
|
});
|
|
951
|
-
} else if (item.ty === "fl" || item.ty === "st") {
|
|
1075
|
+
} else if (item.ty === "fl" || item.ty === "st" || item.ty === "gf") {
|
|
952
1076
|
const preceding = geoms.filter((g) => g.index < i);
|
|
953
1077
|
if (preceding.length === 0) continue;
|
|
954
1078
|
const styleNodes = (hasMerge ? [{
|
|
@@ -1543,9 +1667,11 @@ function decimateLinearKeys(keys, relEps = .002) {
|
|
|
1543
1667
|
* same discipline the importer uses for misaligned parametric geometry.
|
|
1544
1668
|
*
|
|
1545
1669
|
* SCOPE (MVP — mirror the importer's audit discipline: warn + drop, never
|
|
1546
|
-
* silent). IN: Group hierarchy, Rect/Circle/Path with a SOLID fill
|
|
1547
|
-
*
|
|
1548
|
-
*
|
|
1670
|
+
* silent). IN: Group hierarchy, Rect/Circle/Path with a SOLID fill or a
|
|
1671
|
+
* LINEAR/RADIAL gradient `fill: paint` (→ Lottie `gf`; +optional stroke),
|
|
1672
|
+
* transform channels (position / position.x/.y split, opacity, scale,
|
|
1673
|
+
* rotation → identity degrees), animated `fill` color OR animated `fill: paint`
|
|
1674
|
+
* gradient (keyed g/s/e when directly invertible, else sampled), animated `d` path
|
|
1549
1675
|
* (constant topology), and TEXT (ty:5): a Text node → a text layer with a font
|
|
1550
1676
|
* reference (fonts.list) + a text-document keyframe stream (static = one doc;
|
|
1551
1677
|
* animated text/fill/fontSize → doc keyframes sampled on the frame grid, held).
|
|
@@ -1564,8 +1690,9 @@ function decimateLinearKeys(keys, relEps = .002) {
|
|
|
1564
1690
|
* 0.75, not the correct single 0.5). Same limitation the importer documents at
|
|
1565
1691
|
* convert.ts:470-475. A correct-for-overlap precomp (ty:0 + assets) is a later phase.
|
|
1566
1692
|
*
|
|
1567
|
-
* OUT (warned + dropped): Image/Video,
|
|
1568
|
-
*
|
|
1693
|
+
* OUT (warned + dropped): Image/Video, MESH paint (`gf` covers linear/radial only —
|
|
1694
|
+
* mesh has no Lottie ramp), gradient STROKE (`gs` — Path.stroke is a color string,
|
|
1695
|
+
* not a Paint), non-center anchors, text typewriter `reveal`/`revealFraction`, variable-font axes
|
|
1569
1696
|
* (`fontAxes`/`fontVariationSettings` — no Lottie doc field), `box` valign
|
|
1570
1697
|
* (baseline-approximated) and wrap `width` (the player self-reflows), TokenHighlight.
|
|
1571
1698
|
* Animated primitive geometry (width/radius tracks) is SAMPLED, not channel-mapped.
|
|
@@ -2101,11 +2228,7 @@ function colorKeys(ctx, tr) {
|
|
|
2101
2228
|
function buildFill(ctx, node, tracks) {
|
|
2102
2229
|
const tr = tracks.get("fill");
|
|
2103
2230
|
if (tr) {
|
|
2104
|
-
if (tr.type
|
|
2105
|
-
ctx.warn(`${describe(node)}: animated '${tr.type}' fill (gradient/mesh) is not exported — dropped`);
|
|
2106
|
-
return;
|
|
2107
|
-
}
|
|
2108
|
-
return {
|
|
2231
|
+
if (tr.type === "color") return {
|
|
2109
2232
|
ty: "fl",
|
|
2110
2233
|
c: {
|
|
2111
2234
|
a: 1,
|
|
@@ -2116,19 +2239,267 @@ function buildFill(ctx, node, tracks) {
|
|
|
2116
2239
|
k: 100
|
|
2117
2240
|
}
|
|
2118
2241
|
};
|
|
2242
|
+
if (tr.type === "paint") return buildAnimatedGradientFill(ctx, node, tr);
|
|
2243
|
+
ctx.warn(`${describe(node)}: animated '${tr.type}' fill is not exported — dropped`);
|
|
2244
|
+
return;
|
|
2119
2245
|
}
|
|
2120
2246
|
const fill = node.fill();
|
|
2121
|
-
if (typeof fill
|
|
2122
|
-
|
|
2123
|
-
return
|
|
2247
|
+
if (typeof fill === "string") {
|
|
2248
|
+
if (fill === "") return void 0;
|
|
2249
|
+
return {
|
|
2250
|
+
ty: "fl",
|
|
2251
|
+
c: {
|
|
2252
|
+
a: 0,
|
|
2253
|
+
k: colorToLottie(fill)
|
|
2254
|
+
},
|
|
2255
|
+
o: {
|
|
2256
|
+
a: 0,
|
|
2257
|
+
k: 100
|
|
2258
|
+
}
|
|
2259
|
+
};
|
|
2124
2260
|
}
|
|
2125
|
-
if (fill === "") return
|
|
2126
|
-
return {
|
|
2261
|
+
if (fill.kind === "color") return {
|
|
2127
2262
|
ty: "fl",
|
|
2128
2263
|
c: {
|
|
2129
2264
|
a: 0,
|
|
2130
|
-
k: colorToLottie(fill)
|
|
2265
|
+
k: colorToLottie(fill.color)
|
|
2266
|
+
},
|
|
2267
|
+
o: {
|
|
2268
|
+
a: 0,
|
|
2269
|
+
k: 100
|
|
2270
|
+
}
|
|
2271
|
+
};
|
|
2272
|
+
if (fill.kind === "mesh") {
|
|
2273
|
+
ctx.warn(`${describe(node)}: a mesh fill has no Lottie gradient ramp (MVP: solid / linear / radial) — dropped`);
|
|
2274
|
+
return;
|
|
2275
|
+
}
|
|
2276
|
+
warnGradientInterpolation(ctx, node, fill);
|
|
2277
|
+
return gradientFillItem(fill, localBounds(node));
|
|
2278
|
+
}
|
|
2279
|
+
/** Local-space bounds of a shape's fill path — the gradient-geometry default source
|
|
2280
|
+
* (matches raster2d.resolveFill: linear ⇒ vertical bounds sweep, radial ⇒ centre +
|
|
2281
|
+
* half-diagonal). Rect/Circle draw centred at the origin; a Path bounds its anchors. */
|
|
2282
|
+
function localBounds(node) {
|
|
2283
|
+
if (node instanceof Rect) {
|
|
2284
|
+
const w = node.width() / 2;
|
|
2285
|
+
const h = node.height() / 2;
|
|
2286
|
+
return {
|
|
2287
|
+
minX: -w,
|
|
2288
|
+
minY: -h,
|
|
2289
|
+
maxX: w,
|
|
2290
|
+
maxY: h
|
|
2291
|
+
};
|
|
2292
|
+
}
|
|
2293
|
+
if (node instanceof Circle) {
|
|
2294
|
+
const r = node.radius();
|
|
2295
|
+
return {
|
|
2296
|
+
minX: -r,
|
|
2297
|
+
minY: -r,
|
|
2298
|
+
maxX: r,
|
|
2299
|
+
maxY: r
|
|
2300
|
+
};
|
|
2301
|
+
}
|
|
2302
|
+
let minX = Infinity;
|
|
2303
|
+
let minY = Infinity;
|
|
2304
|
+
let maxX = -Infinity;
|
|
2305
|
+
let maxY = -Infinity;
|
|
2306
|
+
for (const contour of node.data()) for (const [x, y] of contour.v) {
|
|
2307
|
+
if (x < minX) minX = x;
|
|
2308
|
+
if (y < minY) minY = y;
|
|
2309
|
+
if (x > maxX) maxX = x;
|
|
2310
|
+
if (y > maxY) maxY = y;
|
|
2311
|
+
}
|
|
2312
|
+
if (!Number.isFinite(minX)) return {
|
|
2313
|
+
minX: 0,
|
|
2314
|
+
minY: 0,
|
|
2315
|
+
maxX: 0,
|
|
2316
|
+
maxY: 0
|
|
2317
|
+
};
|
|
2318
|
+
return {
|
|
2319
|
+
minX,
|
|
2320
|
+
minY,
|
|
2321
|
+
maxX,
|
|
2322
|
+
maxY
|
|
2323
|
+
};
|
|
2324
|
+
}
|
|
2325
|
+
/** gf start point (Lottie `s`) from a gradient's geometry, defaulting to bounds
|
|
2326
|
+
* exactly as raster2d.resolveFill does (linear: [centre-x, minY]; radial: centre). */
|
|
2327
|
+
function gradientStart(g, b) {
|
|
2328
|
+
const cx = (b.minX + b.maxX) / 2;
|
|
2329
|
+
if (g.kind === "linear") return g.from ? [g.from[0], g.from[1]] : [cx, b.minY];
|
|
2330
|
+
const cy = (b.minY + b.maxY) / 2;
|
|
2331
|
+
return g.center ? [g.center[0], g.center[1]] : [cx, cy];
|
|
2332
|
+
}
|
|
2333
|
+
/** gf end point (Lottie `e`): linear ⇒ `to`; radial ⇒ centre + [radius, 0], so the
|
|
2334
|
+
* Lottie radial extent (|s→e|) equals the glissade radius (half-diagonal default). */
|
|
2335
|
+
function gradientEnd(g, b) {
|
|
2336
|
+
if (g.kind === "linear") {
|
|
2337
|
+
const cx = (b.minX + b.maxX) / 2;
|
|
2338
|
+
return g.to ? [g.to[0], g.to[1]] : [cx, b.maxY];
|
|
2339
|
+
}
|
|
2340
|
+
const [cx, cy] = gradientStart(g, b);
|
|
2341
|
+
return [cx + (g.radius !== void 0 ? g.radius : Math.hypot(b.maxX - b.minX, b.maxY - b.minY) / 2), cy];
|
|
2342
|
+
}
|
|
2343
|
+
/** Flatten stops into the Lottie `g.k` array: `[offset,r,g,b, …]` (0–1 floats),
|
|
2344
|
+
* then `[offset,a, …]` alpha stops appended iff any stop is translucent. */
|
|
2345
|
+
function gradientStopArray(stops) {
|
|
2346
|
+
const colors = [];
|
|
2347
|
+
const alphas = [];
|
|
2348
|
+
let anyAlpha = false;
|
|
2349
|
+
for (const s of stops) {
|
|
2350
|
+
const { r, g, b, a } = parseColor(s.color);
|
|
2351
|
+
colors.push(s.offset, r / 255, g / 255, b / 255);
|
|
2352
|
+
alphas.push(s.offset, a);
|
|
2353
|
+
if (a < 1) anyAlpha = true;
|
|
2354
|
+
}
|
|
2355
|
+
return anyAlpha ? [...colors, ...alphas] : colors;
|
|
2356
|
+
}
|
|
2357
|
+
function warnGradientInterpolation(ctx, node, g) {
|
|
2358
|
+
if (g.interpolation !== void 0 && g.interpolation !== "linear") ctx.warn(`${describe(node)}: '${g.interpolation}' gradient interpolation is emitted as a linear Lottie ramp (gf has no smooth/gaussian mode) — mid-stop banding may differ`);
|
|
2359
|
+
}
|
|
2360
|
+
/** A static linear/radial gradient → a `gf` shape item (geometry from `bounds`). */
|
|
2361
|
+
function gradientFillItem(g, bounds) {
|
|
2362
|
+
const gk = {
|
|
2363
|
+
p: g.stops.length,
|
|
2364
|
+
k: {
|
|
2365
|
+
a: 0,
|
|
2366
|
+
k: gradientStopArray(g.stops)
|
|
2367
|
+
}
|
|
2368
|
+
};
|
|
2369
|
+
const s = gradientStart(g, bounds);
|
|
2370
|
+
const e = gradientEnd(g, bounds);
|
|
2371
|
+
if (g.kind === "radial") return {
|
|
2372
|
+
ty: "gf",
|
|
2373
|
+
t: 2,
|
|
2374
|
+
s: {
|
|
2375
|
+
a: 0,
|
|
2376
|
+
k: s
|
|
2377
|
+
},
|
|
2378
|
+
e: {
|
|
2379
|
+
a: 0,
|
|
2380
|
+
k: e
|
|
2381
|
+
},
|
|
2382
|
+
g: gk,
|
|
2383
|
+
h: {
|
|
2384
|
+
a: 0,
|
|
2385
|
+
k: 0
|
|
2386
|
+
},
|
|
2387
|
+
a: {
|
|
2388
|
+
a: 0,
|
|
2389
|
+
k: 0
|
|
2390
|
+
},
|
|
2391
|
+
o: {
|
|
2392
|
+
a: 0,
|
|
2393
|
+
k: 100
|
|
2394
|
+
}
|
|
2395
|
+
};
|
|
2396
|
+
return {
|
|
2397
|
+
ty: "gf",
|
|
2398
|
+
t: 1,
|
|
2399
|
+
s: {
|
|
2400
|
+
a: 0,
|
|
2401
|
+
k: s
|
|
2402
|
+
},
|
|
2403
|
+
e: {
|
|
2404
|
+
a: 0,
|
|
2405
|
+
k: e
|
|
2406
|
+
},
|
|
2407
|
+
g: gk,
|
|
2408
|
+
o: {
|
|
2409
|
+
a: 0,
|
|
2410
|
+
k: 100
|
|
2411
|
+
}
|
|
2412
|
+
};
|
|
2413
|
+
}
|
|
2414
|
+
/**
|
|
2415
|
+
* An animated `fill: paint` track → a `gf` with keyframed s/e/g channels. The
|
|
2416
|
+
* gradient KIND (linear/radial) is fixed to the first key's kind (Lottie can't
|
|
2417
|
+
* animate `t`); mesh/color first keys warn-drop. Directly-invertible tracks keep
|
|
2418
|
+
* their eases via emitKeys; anything else (named ease / spring / expr) samples on
|
|
2419
|
+
* the frame grid. A key whose kind/stop-count differs from the first would snap
|
|
2420
|
+
* under paintType anyway, so its geometry/ramp is read as best-effort.
|
|
2421
|
+
*/
|
|
2422
|
+
function buildAnimatedGradientFill(ctx, node, tr) {
|
|
2423
|
+
const first = tr.keys[0].value;
|
|
2424
|
+
if (first.kind === "mesh") {
|
|
2425
|
+
ctx.warn(`${describe(node)}: an animated mesh fill has no Lottie gradient ramp — dropped`);
|
|
2426
|
+
return;
|
|
2427
|
+
}
|
|
2428
|
+
if (first.kind === "color") {
|
|
2429
|
+
ctx.warn(`${describe(node)}: an animated color-only paint fill is not exported (use a 'color' track) — dropped`);
|
|
2430
|
+
return;
|
|
2431
|
+
}
|
|
2432
|
+
const kind = first.kind;
|
|
2433
|
+
const bounds = localBounds(node);
|
|
2434
|
+
const asGradient = (p) => p.kind === "linear" || p.kind === "radial" ? p : {
|
|
2435
|
+
kind,
|
|
2436
|
+
stops: [{
|
|
2437
|
+
offset: 0,
|
|
2438
|
+
color: "#000000"
|
|
2439
|
+
}]
|
|
2440
|
+
};
|
|
2441
|
+
warnGradientInterpolation(ctx, node, first);
|
|
2442
|
+
const p = first.stops.length;
|
|
2443
|
+
const sMap = (v) => gradientStart(asGradient(v), bounds);
|
|
2444
|
+
const eMap = (v) => gradientEnd(asGradient(v), bounds);
|
|
2445
|
+
const gMap = (v) => gradientStopArray(asGradient(v).stops);
|
|
2446
|
+
let sK;
|
|
2447
|
+
let eK;
|
|
2448
|
+
let gK;
|
|
2449
|
+
if (isDirectlyInvertible(tr.keys, tr.expr)) {
|
|
2450
|
+
sK = emitKeys(tr.keys, ctx.fr, sMap);
|
|
2451
|
+
eK = emitKeys(tr.keys, ctx.fr, eMap);
|
|
2452
|
+
gK = emitKeys(tr.keys, ctx.fr, gMap);
|
|
2453
|
+
} else {
|
|
2454
|
+
ctx.warn(`${describe(node)}: animated gradient fill is sampled at ${ctx.fr} fps (non-invertible ease)`);
|
|
2455
|
+
sK = sampleToLottieKeys(tr, ctx.fr, ctx.ip, ctx.op, sMap);
|
|
2456
|
+
eK = sampleToLottieKeys(tr, ctx.fr, ctx.ip, ctx.op, eMap);
|
|
2457
|
+
gK = sampleToLottieKeys(tr, ctx.fr, ctx.ip, ctx.op, gMap);
|
|
2458
|
+
}
|
|
2459
|
+
const g = {
|
|
2460
|
+
p,
|
|
2461
|
+
k: {
|
|
2462
|
+
a: 1,
|
|
2463
|
+
k: gK
|
|
2464
|
+
}
|
|
2465
|
+
};
|
|
2466
|
+
if (kind === "radial") return {
|
|
2467
|
+
ty: "gf",
|
|
2468
|
+
t: 2,
|
|
2469
|
+
s: {
|
|
2470
|
+
a: 1,
|
|
2471
|
+
k: sK
|
|
2472
|
+
},
|
|
2473
|
+
e: {
|
|
2474
|
+
a: 1,
|
|
2475
|
+
k: eK
|
|
2476
|
+
},
|
|
2477
|
+
g,
|
|
2478
|
+
h: {
|
|
2479
|
+
a: 0,
|
|
2480
|
+
k: 0
|
|
2481
|
+
},
|
|
2482
|
+
a: {
|
|
2483
|
+
a: 0,
|
|
2484
|
+
k: 0
|
|
2485
|
+
},
|
|
2486
|
+
o: {
|
|
2487
|
+
a: 0,
|
|
2488
|
+
k: 100
|
|
2489
|
+
}
|
|
2490
|
+
};
|
|
2491
|
+
return {
|
|
2492
|
+
ty: "gf",
|
|
2493
|
+
t: 1,
|
|
2494
|
+
s: {
|
|
2495
|
+
a: 1,
|
|
2496
|
+
k: sK
|
|
2497
|
+
},
|
|
2498
|
+
e: {
|
|
2499
|
+
a: 1,
|
|
2500
|
+
k: eK
|
|
2131
2501
|
},
|
|
2502
|
+
g,
|
|
2132
2503
|
o: {
|
|
2133
2504
|
a: 0,
|
|
2134
2505
|
k: 100
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glissade/lottie",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.51.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.
|
|
22
|
-
"@glissade/scene": "0.
|
|
21
|
+
"@glissade/core": "0.51.0-pre.1",
|
|
22
|
+
"@glissade/scene": "0.51.0-pre.1"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@glissade/backend-skia": "0.
|
|
25
|
+
"@glissade/backend-skia": "0.51.0-pre.1"
|
|
26
26
|
},
|
|
27
27
|
"repository": {
|
|
28
28
|
"type": "git",
|