@a-company/atelier 0.29.0 → 0.36.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/chunk-5QQESXI6.js +4432 -0
- package/dist/chunk-5QQESXI6.js.map +1 -0
- package/dist/cli.cjs +2391 -530
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +301 -429
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +2233 -38
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +584 -2
- package/dist/index.d.ts +584 -2
- package/dist/index.js +111 -3
- package/dist/mcp.cjs +1215 -365
- package/dist/mcp.cjs.map +1 -1
- package/dist/mcp.js +1209 -365
- package/dist/mcp.js.map +1 -1
- package/package.json +13 -7
- package/src/web/inline-app.ts +867 -0
- package/src/web/tsconfig.json +9 -0
- package/templates/welcome.atelier +67 -0
- package/dist/chunk-JV7RGETS.js +0 -2292
- package/dist/chunk-JV7RGETS.js.map +0 -1
package/dist/chunk-JV7RGETS.js
DELETED
|
@@ -1,2292 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ImageCache,
|
|
3
|
-
buildEffectiveLayer,
|
|
4
|
-
findTemplateVariables,
|
|
5
|
-
renderFrame,
|
|
6
|
-
resolveFrame,
|
|
7
|
-
validateAllDeltas
|
|
8
|
-
} from "./chunk-JPZ4F4PW.js";
|
|
9
|
-
|
|
10
|
-
// src/commands/validate.ts
|
|
11
|
-
import { readFileSync } from "fs";
|
|
12
|
-
import { resolve } from "path";
|
|
13
|
-
|
|
14
|
-
// ../schema/dist/index.js
|
|
15
|
-
import { z } from "zod";
|
|
16
|
-
import { z as z2 } from "zod";
|
|
17
|
-
import { z as z3 } from "zod";
|
|
18
|
-
import { z as z4 } from "zod";
|
|
19
|
-
import { z as z5 } from "zod";
|
|
20
|
-
import { z as z6 } from "zod";
|
|
21
|
-
import { z as z8 } from "zod";
|
|
22
|
-
import { z as z7 } from "zod";
|
|
23
|
-
import { z as z9 } from "zod";
|
|
24
|
-
import { z as z10 } from "zod";
|
|
25
|
-
import { z as z11 } from "zod";
|
|
26
|
-
import { z as z12 } from "zod";
|
|
27
|
-
import { z as z13 } from "zod";
|
|
28
|
-
import { z as z14 } from "zod";
|
|
29
|
-
import { parse as yamlParse, stringify as yamlStringify } from "yaml";
|
|
30
|
-
var PixelSchema = z.number();
|
|
31
|
-
var PercentageSchema = z.string().regex(/^-?\d+(\.\d+)?%$/, {
|
|
32
|
-
message: 'Percentage must be a number followed by %, e.g. "50%"'
|
|
33
|
-
});
|
|
34
|
-
var UnitValueSchema = z.union([PixelSchema, PercentageSchema]);
|
|
35
|
-
var FrameSchema = z2.object({
|
|
36
|
-
x: UnitValueSchema,
|
|
37
|
-
y: UnitValueSchema
|
|
38
|
-
});
|
|
39
|
-
var BoundsSchema = z2.object({
|
|
40
|
-
width: UnitValueSchema,
|
|
41
|
-
height: UnitValueSchema
|
|
42
|
-
});
|
|
43
|
-
var AnchorPointSchema = z2.object({
|
|
44
|
-
x: z2.number().min(0).max(1),
|
|
45
|
-
y: z2.number().min(0).max(1)
|
|
46
|
-
});
|
|
47
|
-
var RGBAColorSchema = z3.object({
|
|
48
|
-
r: z3.number().min(0).max(255),
|
|
49
|
-
g: z3.number().min(0).max(255),
|
|
50
|
-
b: z3.number().min(0).max(255),
|
|
51
|
-
a: z3.number().min(0).max(1)
|
|
52
|
-
});
|
|
53
|
-
var HSLAColorSchema = z3.object({
|
|
54
|
-
h: z3.number().min(0).max(360),
|
|
55
|
-
s: z3.number().min(0).max(100),
|
|
56
|
-
l: z3.number().min(0).max(100),
|
|
57
|
-
a: z3.number().min(0).max(1)
|
|
58
|
-
});
|
|
59
|
-
var HexColorSchema = z3.string().regex(/^#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/, {
|
|
60
|
-
message: "Color must be a hex string: #RGB, #RGBA, #RRGGBB, or #RRGGBBAA"
|
|
61
|
-
});
|
|
62
|
-
var ColorSchema = z3.union([RGBAColorSchema, HSLAColorSchema, HexColorSchema]);
|
|
63
|
-
var PathPointSchema = z4.object({
|
|
64
|
-
x: z4.number(),
|
|
65
|
-
y: z4.number(),
|
|
66
|
-
in: z4.object({ x: z4.number(), y: z4.number() }).optional(),
|
|
67
|
-
out: z4.object({ x: z4.number(), y: z4.number() }).optional()
|
|
68
|
-
});
|
|
69
|
-
var RectShapeSchema = z4.object({
|
|
70
|
-
type: z4.literal("rect"),
|
|
71
|
-
cornerRadius: z4.union([
|
|
72
|
-
z4.number().min(0),
|
|
73
|
-
z4.tuple([z4.number().min(0), z4.number().min(0), z4.number().min(0), z4.number().min(0)])
|
|
74
|
-
]).optional()
|
|
75
|
-
});
|
|
76
|
-
var EllipseShapeSchema = z4.object({
|
|
77
|
-
type: z4.literal("ellipse")
|
|
78
|
-
});
|
|
79
|
-
var PathShapeSchema = z4.object({
|
|
80
|
-
type: z4.literal("path"),
|
|
81
|
-
points: z4.array(PathPointSchema).min(2, "Path must have at least 2 points"),
|
|
82
|
-
closed: z4.boolean().optional()
|
|
83
|
-
});
|
|
84
|
-
var ShapeSchema = z4.discriminatedUnion("type", [
|
|
85
|
-
RectShapeSchema,
|
|
86
|
-
EllipseShapeSchema,
|
|
87
|
-
PathShapeSchema
|
|
88
|
-
]);
|
|
89
|
-
var GradientStopSchema = z4.object({
|
|
90
|
-
offset: z4.number().min(0).max(1),
|
|
91
|
-
color: ColorSchema
|
|
92
|
-
});
|
|
93
|
-
var SolidFillSchema = z4.object({
|
|
94
|
-
type: z4.literal("solid"),
|
|
95
|
-
color: ColorSchema
|
|
96
|
-
});
|
|
97
|
-
var LinearGradientFillSchema = z4.object({
|
|
98
|
-
type: z4.literal("linear-gradient"),
|
|
99
|
-
angle: z4.number(),
|
|
100
|
-
stops: z4.array(GradientStopSchema).min(2, "Gradient needs at least 2 stops")
|
|
101
|
-
});
|
|
102
|
-
var RadialGradientFillSchema = z4.object({
|
|
103
|
-
type: z4.literal("radial-gradient"),
|
|
104
|
-
center: z4.object({ x: UnitValueSchema, y: UnitValueSchema }),
|
|
105
|
-
radius: UnitValueSchema,
|
|
106
|
-
stops: z4.array(GradientStopSchema).min(2, "Gradient needs at least 2 stops")
|
|
107
|
-
});
|
|
108
|
-
var FillSchema = z4.discriminatedUnion("type", [
|
|
109
|
-
SolidFillSchema,
|
|
110
|
-
LinearGradientFillSchema,
|
|
111
|
-
RadialGradientFillSchema
|
|
112
|
-
]);
|
|
113
|
-
var StrokeSchema = z4.object({
|
|
114
|
-
color: ColorSchema,
|
|
115
|
-
width: z4.number().min(0),
|
|
116
|
-
dash: z4.array(z4.number().min(0)).optional(),
|
|
117
|
-
lineCap: z4.enum(["butt", "round", "square"]).optional(),
|
|
118
|
-
lineJoin: z4.enum(["miter", "round", "bevel"]).optional(),
|
|
119
|
-
strokeStart: z4.number().min(0).max(1).optional(),
|
|
120
|
-
strokeEnd: z4.number().min(0).max(1).optional()
|
|
121
|
-
});
|
|
122
|
-
var TextStyleSchema = z4.object({
|
|
123
|
-
fontFamily: z4.string().min(1, "fontFamily is required"),
|
|
124
|
-
fontSize: z4.number().positive("fontSize must be positive"),
|
|
125
|
-
fontWeight: z4.union([z4.number(), z4.enum(["normal", "bold"])]).optional(),
|
|
126
|
-
fontStyle: z4.enum(["normal", "italic"]).optional(),
|
|
127
|
-
textAlign: z4.enum(["left", "center", "right"]).optional(),
|
|
128
|
-
lineHeight: z4.number().positive().optional(),
|
|
129
|
-
letterSpacing: z4.number().optional(),
|
|
130
|
-
color: ColorSchema
|
|
131
|
-
});
|
|
132
|
-
var LinearEasingSchema = z5.object({ type: z5.literal("linear") });
|
|
133
|
-
var CubicBezierEasingSchema = z5.object({
|
|
134
|
-
type: z5.literal("cubic-bezier"),
|
|
135
|
-
x1: z5.number().min(0).max(1),
|
|
136
|
-
y1: z5.number(),
|
|
137
|
-
x2: z5.number().min(0).max(1),
|
|
138
|
-
y2: z5.number()
|
|
139
|
-
});
|
|
140
|
-
var SpringEasingSchema = z5.object({
|
|
141
|
-
type: z5.literal("spring"),
|
|
142
|
-
mass: z5.number().positive().optional(),
|
|
143
|
-
stiffness: z5.number().positive().optional(),
|
|
144
|
-
damping: z5.number().positive().optional(),
|
|
145
|
-
velocity: z5.number().optional()
|
|
146
|
-
});
|
|
147
|
-
var StepEasingSchema = z5.object({
|
|
148
|
-
type: z5.literal("step"),
|
|
149
|
-
steps: z5.number().int().positive(),
|
|
150
|
-
position: z5.enum(["start", "end"]).optional()
|
|
151
|
-
});
|
|
152
|
-
var EasingPresetSchema = z5.enum(["linear", "ease-in", "ease-out", "ease-in-out"]);
|
|
153
|
-
var EasingSchema = z5.union([
|
|
154
|
-
LinearEasingSchema,
|
|
155
|
-
CubicBezierEasingSchema,
|
|
156
|
-
SpringEasingSchema,
|
|
157
|
-
StepEasingSchema,
|
|
158
|
-
EasingPresetSchema
|
|
159
|
-
]);
|
|
160
|
-
var ShadowSchema = z6.object({
|
|
161
|
-
color: ColorSchema,
|
|
162
|
-
blur: z6.number().min(0),
|
|
163
|
-
offsetX: z6.number().optional(),
|
|
164
|
-
offsetY: z6.number().optional()
|
|
165
|
-
});
|
|
166
|
-
var TriggerTypeSchema = z7.enum([
|
|
167
|
-
"click",
|
|
168
|
-
"hover",
|
|
169
|
-
"pointerdown",
|
|
170
|
-
"pointerup",
|
|
171
|
-
"timer",
|
|
172
|
-
"signal"
|
|
173
|
-
]);
|
|
174
|
-
var TriggerSchema = z7.object({
|
|
175
|
-
type: TriggerTypeSchema,
|
|
176
|
-
delay: z7.number().nonnegative("Timer delay must be non-negative").optional(),
|
|
177
|
-
signal: z7.string().optional()
|
|
178
|
-
}).refine(
|
|
179
|
-
(t) => t.type !== "timer" || t.delay !== void 0,
|
|
180
|
-
{ message: "Timer trigger requires a delay" }
|
|
181
|
-
).refine(
|
|
182
|
-
(t) => t.type !== "signal" || t.signal !== void 0,
|
|
183
|
-
{ message: "Signal trigger requires a signal name" }
|
|
184
|
-
);
|
|
185
|
-
var ActionTypeSchema = z7.enum([
|
|
186
|
-
"go-to-state",
|
|
187
|
-
"emit-signal",
|
|
188
|
-
"set-variable",
|
|
189
|
-
"toggle-visibility"
|
|
190
|
-
]);
|
|
191
|
-
var ActionSchema = z7.object({
|
|
192
|
-
type: ActionTypeSchema,
|
|
193
|
-
state: z7.string().optional(),
|
|
194
|
-
signal: z7.string().optional(),
|
|
195
|
-
variable: z7.string().optional(),
|
|
196
|
-
value: z7.unknown().optional(),
|
|
197
|
-
targetLayer: z7.string().optional()
|
|
198
|
-
}).refine(
|
|
199
|
-
(a) => a.type !== "go-to-state" || a.state !== void 0,
|
|
200
|
-
{ message: "go-to-state action requires a state name" }
|
|
201
|
-
).refine(
|
|
202
|
-
(a) => a.type !== "emit-signal" || a.signal !== void 0,
|
|
203
|
-
{ message: "emit-signal action requires a signal name" }
|
|
204
|
-
).refine(
|
|
205
|
-
(a) => a.type !== "set-variable" || a.variable !== void 0 && a.value !== void 0,
|
|
206
|
-
{ message: "set-variable action requires variable and value" }
|
|
207
|
-
);
|
|
208
|
-
var InteractionSchema = z7.object({
|
|
209
|
-
id: z7.string().min(1, "Interaction id is required"),
|
|
210
|
-
trigger: TriggerSchema,
|
|
211
|
-
action: ActionSchema,
|
|
212
|
-
description: z7.string().optional()
|
|
213
|
-
});
|
|
214
|
-
var BlendModeSchema = z8.enum([
|
|
215
|
-
"normal",
|
|
216
|
-
"multiply",
|
|
217
|
-
"screen",
|
|
218
|
-
"overlay",
|
|
219
|
-
"darken",
|
|
220
|
-
"lighten",
|
|
221
|
-
"color-dodge",
|
|
222
|
-
"color-burn",
|
|
223
|
-
"hard-light",
|
|
224
|
-
"soft-light",
|
|
225
|
-
"difference",
|
|
226
|
-
"exclusion",
|
|
227
|
-
"hue",
|
|
228
|
-
"saturation",
|
|
229
|
-
"color",
|
|
230
|
-
"luminosity"
|
|
231
|
-
]);
|
|
232
|
-
var MotionPathSchema = z8.object({
|
|
233
|
-
points: z8.array(PathPointSchema).min(2, "Motion path must have at least 2 points"),
|
|
234
|
-
closed: z8.boolean().optional(),
|
|
235
|
-
autoRotate: z8.boolean().optional(),
|
|
236
|
-
autoRotateOffset: z8.number().optional()
|
|
237
|
-
});
|
|
238
|
-
var ShapeVisualSchema = z8.object({
|
|
239
|
-
type: z8.literal("shape"),
|
|
240
|
-
shape: ShapeSchema,
|
|
241
|
-
fill: FillSchema.optional(),
|
|
242
|
-
stroke: StrokeSchema.optional()
|
|
243
|
-
});
|
|
244
|
-
var TextVisualSchema = z8.object({
|
|
245
|
-
type: z8.literal("text"),
|
|
246
|
-
content: z8.string(),
|
|
247
|
-
style: TextStyleSchema
|
|
248
|
-
});
|
|
249
|
-
var SpritesheetConfigSchema = z8.object({
|
|
250
|
-
columns: z8.number().int().positive(),
|
|
251
|
-
rows: z8.number().int().positive(),
|
|
252
|
-
frameCount: z8.number().int().positive().optional(),
|
|
253
|
-
frameWidth: z8.number().positive(),
|
|
254
|
-
frameHeight: z8.number().positive()
|
|
255
|
-
});
|
|
256
|
-
var SourceRectSchema = z8.object({
|
|
257
|
-
x: z8.number(),
|
|
258
|
-
y: z8.number(),
|
|
259
|
-
width: z8.number().positive(),
|
|
260
|
-
height: z8.number().positive()
|
|
261
|
-
});
|
|
262
|
-
var ImageVisualSchema = z8.object({
|
|
263
|
-
type: z8.literal("image"),
|
|
264
|
-
assetId: z8.string().min(1, "assetId is required"),
|
|
265
|
-
src: z8.string().optional(),
|
|
266
|
-
sourceRect: SourceRectSchema.optional(),
|
|
267
|
-
spritesheet: SpritesheetConfigSchema.optional(),
|
|
268
|
-
frameIndex: z8.number().int().min(0).optional()
|
|
269
|
-
});
|
|
270
|
-
var VideoVisualSchema = z8.object({
|
|
271
|
-
type: z8.literal("video"),
|
|
272
|
-
assetId: z8.string().min(1, "assetId is required"),
|
|
273
|
-
src: z8.string().optional(),
|
|
274
|
-
startFrame: z8.number().int().min(0).optional(),
|
|
275
|
-
sourceOffset: z8.number().min(0).optional(),
|
|
276
|
-
sourceEnd: z8.number().positive().optional(),
|
|
277
|
-
playbackRate: z8.number().positive().optional(),
|
|
278
|
-
volume: z8.number().min(0).max(1).optional(),
|
|
279
|
-
muted: z8.boolean().optional(),
|
|
280
|
-
objectFit: z8.enum(["contain", "cover", "fill"]).optional()
|
|
281
|
-
});
|
|
282
|
-
var GroupVisualSchema = z8.object({
|
|
283
|
-
type: z8.literal("group")
|
|
284
|
-
});
|
|
285
|
-
var RefVisualSchema = z8.object({
|
|
286
|
-
type: z8.literal("ref"),
|
|
287
|
-
src: z8.string().min(1, "src is required"),
|
|
288
|
-
state: z8.string().optional(),
|
|
289
|
-
frame: z8.number().int().min(0).optional()
|
|
290
|
-
});
|
|
291
|
-
var VisualSchema = z8.discriminatedUnion("type", [
|
|
292
|
-
ShapeVisualSchema,
|
|
293
|
-
TextVisualSchema,
|
|
294
|
-
ImageVisualSchema,
|
|
295
|
-
VideoVisualSchema,
|
|
296
|
-
GroupVisualSchema,
|
|
297
|
-
RefVisualSchema
|
|
298
|
-
]);
|
|
299
|
-
var LayerSchema = z8.object({
|
|
300
|
-
id: z8.string().min(1, "Layer id is required"),
|
|
301
|
-
description: z8.string().optional(),
|
|
302
|
-
tags: z8.array(z8.string()).optional(),
|
|
303
|
-
visual: VisualSchema,
|
|
304
|
-
frame: FrameSchema,
|
|
305
|
-
bounds: BoundsSchema,
|
|
306
|
-
anchorPoint: AnchorPointSchema.optional(),
|
|
307
|
-
parentId: z8.string().optional(),
|
|
308
|
-
opacity: z8.number().min(0).max(1).optional(),
|
|
309
|
-
rotation: z8.number().optional(),
|
|
310
|
-
scale: z8.object({ x: z8.number(), y: z8.number() }).optional(),
|
|
311
|
-
visible: z8.boolean().optional(),
|
|
312
|
-
shadow: ShadowSchema.optional(),
|
|
313
|
-
blendMode: BlendModeSchema.optional(),
|
|
314
|
-
motionPath: MotionPathSchema.optional(),
|
|
315
|
-
clipPath: ShapeSchema.optional(),
|
|
316
|
-
tint: z8.object({
|
|
317
|
-
color: z8.string(),
|
|
318
|
-
amount: z8.number().min(0).max(1)
|
|
319
|
-
}).optional(),
|
|
320
|
-
interactions: z8.array(InteractionSchema).optional()
|
|
321
|
-
});
|
|
322
|
-
var AnimatablePropertySchema = z9.enum([
|
|
323
|
-
"frame.x",
|
|
324
|
-
"frame.y",
|
|
325
|
-
"bounds.width",
|
|
326
|
-
"bounds.height",
|
|
327
|
-
"opacity",
|
|
328
|
-
"rotation",
|
|
329
|
-
"scale.x",
|
|
330
|
-
"scale.y",
|
|
331
|
-
"anchorPoint.x",
|
|
332
|
-
"anchorPoint.y",
|
|
333
|
-
"visual.shape.cornerRadius",
|
|
334
|
-
"visual.fill.color",
|
|
335
|
-
"visual.stroke.color",
|
|
336
|
-
"visual.stroke.width",
|
|
337
|
-
"visual.stroke.start",
|
|
338
|
-
"visual.stroke.end",
|
|
339
|
-
"visual.style.fontSize",
|
|
340
|
-
"visual.style.color",
|
|
341
|
-
"shadow.color",
|
|
342
|
-
"shadow.blur",
|
|
343
|
-
"shadow.offsetX",
|
|
344
|
-
"shadow.offsetY",
|
|
345
|
-
"motionPath.progress",
|
|
346
|
-
"visual.fill.angle",
|
|
347
|
-
"visual.fill.center.x",
|
|
348
|
-
"visual.fill.center.y",
|
|
349
|
-
"visual.fill.radius",
|
|
350
|
-
"visual.image.sourceRect.x",
|
|
351
|
-
"visual.image.sourceRect.y",
|
|
352
|
-
"visual.image.sourceRect.width",
|
|
353
|
-
"visual.image.sourceRect.height",
|
|
354
|
-
"visual.image.frameIndex",
|
|
355
|
-
"visible",
|
|
356
|
-
"tint.color",
|
|
357
|
-
"tint.amount"
|
|
358
|
-
]);
|
|
359
|
-
var FrameRangeSchema = z9.tuple([
|
|
360
|
-
z9.number().int().min(0, "Frame start must be >= 0"),
|
|
361
|
-
z9.number().int().min(0, "Frame end must be >= 0")
|
|
362
|
-
]).refine(([start, end]) => end >= start, {
|
|
363
|
-
message: "Frame range end must be >= start"
|
|
364
|
-
});
|
|
365
|
-
var DeltaSchema = z9.object({
|
|
366
|
-
id: z9.string().optional(),
|
|
367
|
-
name: z9.string().optional(),
|
|
368
|
-
layer: z9.string().min(1, "Delta must reference a layer id"),
|
|
369
|
-
property: AnimatablePropertySchema,
|
|
370
|
-
range: FrameRangeSchema,
|
|
371
|
-
from: z9.unknown(),
|
|
372
|
-
to: z9.unknown(),
|
|
373
|
-
easing: EasingSchema.optional(),
|
|
374
|
-
description: z9.string().optional(),
|
|
375
|
-
tags: z9.array(z9.string()).optional()
|
|
376
|
-
});
|
|
377
|
-
var AudioSchema = z10.object({
|
|
378
|
-
src: z10.string().min(1, "Audio src is required"),
|
|
379
|
-
offset: z10.number().min(0, "Audio offset must be non-negative").optional(),
|
|
380
|
-
volume: z10.number().min(0).max(1, "Audio volume must be 0\u20131").optional(),
|
|
381
|
-
loop: z10.boolean().optional(),
|
|
382
|
-
startFrame: z10.number().int().min(0, "Audio startFrame must be a non-negative integer").optional()
|
|
383
|
-
});
|
|
384
|
-
var StateTransitionConfigSchema = z10.object({
|
|
385
|
-
duration: z10.number().int().positive("Transition duration must be a positive integer (frames)"),
|
|
386
|
-
easing: EasingSchema.optional()
|
|
387
|
-
});
|
|
388
|
-
var StateSchema = z10.object({
|
|
389
|
-
description: z10.string().optional(),
|
|
390
|
-
tags: z10.array(z10.string()).optional(),
|
|
391
|
-
parent: z10.string().optional(),
|
|
392
|
-
duration: z10.number().int().positive("State duration must be a positive integer (frames)"),
|
|
393
|
-
deltas: z10.array(DeltaSchema),
|
|
394
|
-
audio: AudioSchema.optional(),
|
|
395
|
-
transitions: z10.record(z10.string(), StateTransitionConfigSchema).optional()
|
|
396
|
-
});
|
|
397
|
-
var PresetDeltaSchema = z11.object({
|
|
398
|
-
property: AnimatablePropertySchema,
|
|
399
|
-
offset: z11.tuple([z11.number().int().min(0), z11.number().int().min(0)]).optional(),
|
|
400
|
-
from: z11.unknown(),
|
|
401
|
-
to: z11.unknown(),
|
|
402
|
-
easing: EasingSchema.optional()
|
|
403
|
-
});
|
|
404
|
-
var PresetSchema = z11.object({
|
|
405
|
-
description: z11.string().optional(),
|
|
406
|
-
tags: z11.array(z11.string()).optional(),
|
|
407
|
-
deltas: z11.array(PresetDeltaSchema).min(1, "Preset must have at least one delta")
|
|
408
|
-
});
|
|
409
|
-
var VariableTypeSchema = z12.enum(["string", "number", "color", "asset", "boolean"]);
|
|
410
|
-
var VariableSchema = z12.object({
|
|
411
|
-
type: VariableTypeSchema,
|
|
412
|
-
default: z12.unknown().optional(),
|
|
413
|
-
description: z12.string().optional()
|
|
414
|
-
});
|
|
415
|
-
var AssetTypeSchema = z13.enum(["image", "svg", "font", "animation", "audio", "video"]);
|
|
416
|
-
var AssetSchema = z13.object({
|
|
417
|
-
type: AssetTypeSchema,
|
|
418
|
-
src: z13.string().min(1, "Asset src is required"),
|
|
419
|
-
description: z13.string().optional(),
|
|
420
|
-
spritesheet: z13.object({
|
|
421
|
-
columns: z13.number().int().positive(),
|
|
422
|
-
rows: z13.number().int().positive(),
|
|
423
|
-
frameCount: z13.number().int().positive().optional(),
|
|
424
|
-
frameWidth: z13.number().positive(),
|
|
425
|
-
frameHeight: z13.number().positive()
|
|
426
|
-
}).optional(),
|
|
427
|
-
videoMeta: z13.object({
|
|
428
|
-
duration: z13.number().positive("videoMeta.duration must be positive"),
|
|
429
|
-
fps: z13.number().positive("videoMeta.fps must be positive"),
|
|
430
|
-
width: z13.number().int().positive(),
|
|
431
|
-
height: z13.number().int().positive()
|
|
432
|
-
}).optional()
|
|
433
|
-
});
|
|
434
|
-
var CanvasSchema = z14.object({
|
|
435
|
-
width: z14.number().int().positive("Canvas width must be a positive integer"),
|
|
436
|
-
height: z14.number().int().positive("Canvas height must be a positive integer"),
|
|
437
|
-
fps: z14.number().int().positive("FPS must be a positive integer"),
|
|
438
|
-
background: z14.string().optional()
|
|
439
|
-
});
|
|
440
|
-
var AtelierDocumentSchema = z14.object({
|
|
441
|
-
version: z14.string().min(1, "Version is required"),
|
|
442
|
-
name: z14.string().min(1, "Animation name is required"),
|
|
443
|
-
description: z14.string().optional(),
|
|
444
|
-
tags: z14.array(z14.string()).optional(),
|
|
445
|
-
canvas: CanvasSchema,
|
|
446
|
-
variables: z14.record(z14.string(), VariableSchema).optional(),
|
|
447
|
-
assets: z14.record(z14.string(), AssetSchema).optional(),
|
|
448
|
-
presets: z14.record(z14.string(), PresetSchema).optional(),
|
|
449
|
-
layers: z14.array(LayerSchema),
|
|
450
|
-
states: z14.record(z14.string(), StateSchema)
|
|
451
|
-
});
|
|
452
|
-
function formatErrors(error) {
|
|
453
|
-
return error.issues.map((issue) => ({
|
|
454
|
-
path: issue.path.join(".") || "(root)",
|
|
455
|
-
message: issue.message
|
|
456
|
-
}));
|
|
457
|
-
}
|
|
458
|
-
function validateDocument(input) {
|
|
459
|
-
const result = AtelierDocumentSchema.safeParse(input);
|
|
460
|
-
if (result.success) {
|
|
461
|
-
return { success: true, data: result.data };
|
|
462
|
-
}
|
|
463
|
-
return { success: false, errors: formatErrors(result.error) };
|
|
464
|
-
}
|
|
465
|
-
function validateVideoLayer(visual, videoMetaDuration) {
|
|
466
|
-
const errors = [];
|
|
467
|
-
if (!visual.assetId) {
|
|
468
|
-
errors.push({ path: "assetId", message: "assetId is required" });
|
|
469
|
-
}
|
|
470
|
-
const sourceOffset = visual.sourceOffset ?? 0;
|
|
471
|
-
if (visual.sourceEnd !== void 0) {
|
|
472
|
-
if (visual.sourceEnd <= sourceOffset) {
|
|
473
|
-
errors.push({
|
|
474
|
-
path: "sourceEnd",
|
|
475
|
-
message: `sourceEnd (${visual.sourceEnd}) must be greater than sourceOffset (${sourceOffset})`
|
|
476
|
-
});
|
|
477
|
-
}
|
|
478
|
-
if (videoMetaDuration !== void 0 && visual.sourceEnd > videoMetaDuration) {
|
|
479
|
-
errors.push({
|
|
480
|
-
path: "sourceEnd",
|
|
481
|
-
message: `sourceEnd (${visual.sourceEnd}) exceeds asset duration (${videoMetaDuration})`
|
|
482
|
-
});
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
if (videoMetaDuration !== void 0 && sourceOffset >= videoMetaDuration) {
|
|
486
|
-
errors.push({
|
|
487
|
-
path: "sourceOffset",
|
|
488
|
-
message: `sourceOffset (${sourceOffset}) is at or beyond asset duration (${videoMetaDuration})`
|
|
489
|
-
});
|
|
490
|
-
}
|
|
491
|
-
if (errors.length > 0) return { success: false, errors };
|
|
492
|
-
return { success: true, data: visual };
|
|
493
|
-
}
|
|
494
|
-
function parseAtelier(yamlString) {
|
|
495
|
-
let parsed;
|
|
496
|
-
try {
|
|
497
|
-
parsed = yamlParse(yamlString);
|
|
498
|
-
} catch (err) {
|
|
499
|
-
return {
|
|
500
|
-
success: false,
|
|
501
|
-
errors: [{ path: "(yaml)", message: `YAML parse error: ${err.message}` }]
|
|
502
|
-
};
|
|
503
|
-
}
|
|
504
|
-
return validateDocument(parsed);
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
// src/commands/validate.ts
|
|
508
|
-
function validateFile(filePath) {
|
|
509
|
-
const absPath = resolve(filePath);
|
|
510
|
-
let content;
|
|
511
|
-
try {
|
|
512
|
-
content = readFileSync(absPath, "utf-8");
|
|
513
|
-
} catch {
|
|
514
|
-
return { valid: false, errors: [`Cannot read file: ${absPath}`] };
|
|
515
|
-
}
|
|
516
|
-
const result = parseAtelier(content);
|
|
517
|
-
if (!result.success) {
|
|
518
|
-
return {
|
|
519
|
-
valid: false,
|
|
520
|
-
errors: result.errors.map(
|
|
521
|
-
(e) => `${e.path}: ${e.message}`
|
|
522
|
-
)
|
|
523
|
-
};
|
|
524
|
-
}
|
|
525
|
-
const overlapErrors = [];
|
|
526
|
-
for (const [stateName, state] of Object.entries(result.data.states)) {
|
|
527
|
-
const overlaps = validateAllDeltas(state.deltas);
|
|
528
|
-
for (const overlap of overlaps) {
|
|
529
|
-
overlapErrors.push(`State "${stateName}": ${overlap.message}`);
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
if (overlapErrors.length > 0) {
|
|
533
|
-
return { valid: false, errors: overlapErrors };
|
|
534
|
-
}
|
|
535
|
-
return { valid: true, errors: [] };
|
|
536
|
-
}
|
|
537
|
-
function validateCommand(program) {
|
|
538
|
-
program.command("validate <file>").description("Validate an .atelier YAML file").action((file) => {
|
|
539
|
-
const { valid, errors } = validateFile(file);
|
|
540
|
-
if (valid) {
|
|
541
|
-
console.log("Valid");
|
|
542
|
-
} else {
|
|
543
|
-
console.error("Validation errors:");
|
|
544
|
-
for (const error of errors) {
|
|
545
|
-
console.error(` - ${error}`);
|
|
546
|
-
}
|
|
547
|
-
process.exit(1);
|
|
548
|
-
}
|
|
549
|
-
});
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
// src/commands/info.ts
|
|
553
|
-
import { readFileSync as readFileSync2 } from "fs";
|
|
554
|
-
import { resolve as resolve2 } from "path";
|
|
555
|
-
function getInfo(doc) {
|
|
556
|
-
return {
|
|
557
|
-
name: doc.name,
|
|
558
|
-
description: doc.description,
|
|
559
|
-
canvas: {
|
|
560
|
-
width: doc.canvas.width,
|
|
561
|
-
height: doc.canvas.height,
|
|
562
|
-
fps: doc.canvas.fps,
|
|
563
|
-
background: doc.canvas.background
|
|
564
|
-
},
|
|
565
|
-
layers: {
|
|
566
|
-
count: doc.layers.length,
|
|
567
|
-
items: doc.layers.map((layer) => ({
|
|
568
|
-
id: layer.id,
|
|
569
|
-
type: layer.visual.type
|
|
570
|
-
}))
|
|
571
|
-
},
|
|
572
|
-
states: {
|
|
573
|
-
count: Object.keys(doc.states).length,
|
|
574
|
-
items: Object.entries(doc.states).map(([name, state]) => ({
|
|
575
|
-
name,
|
|
576
|
-
duration: state.duration,
|
|
577
|
-
deltaCount: state.deltas.length
|
|
578
|
-
}))
|
|
579
|
-
},
|
|
580
|
-
presets: {
|
|
581
|
-
count: doc.presets ? Object.keys(doc.presets).length : 0
|
|
582
|
-
}
|
|
583
|
-
};
|
|
584
|
-
}
|
|
585
|
-
function formatInfo(info) {
|
|
586
|
-
const lines = [];
|
|
587
|
-
lines.push(`Name: ${info.name}`);
|
|
588
|
-
if (info.description) {
|
|
589
|
-
lines.push(`Description: ${info.description}`);
|
|
590
|
-
}
|
|
591
|
-
const bg = info.canvas.background ? `, background: ${info.canvas.background}` : "";
|
|
592
|
-
lines.push(
|
|
593
|
-
`Canvas: ${info.canvas.width}x${info.canvas.height} @ ${info.canvas.fps}fps${bg}`
|
|
594
|
-
);
|
|
595
|
-
lines.push(`Layers: ${info.layers.count}`);
|
|
596
|
-
for (const layer of info.layers.items) {
|
|
597
|
-
lines.push(` - ${layer.id} (${layer.type})`);
|
|
598
|
-
}
|
|
599
|
-
lines.push(`States: ${info.states.count}`);
|
|
600
|
-
for (const state of info.states.items) {
|
|
601
|
-
lines.push(
|
|
602
|
-
` - ${state.name}: ${state.duration} frames, ${state.deltaCount} deltas`
|
|
603
|
-
);
|
|
604
|
-
}
|
|
605
|
-
if (info.presets.count > 0) {
|
|
606
|
-
lines.push(`Presets: ${info.presets.count}`);
|
|
607
|
-
}
|
|
608
|
-
return lines.join("\n");
|
|
609
|
-
}
|
|
610
|
-
function readAndParse(file) {
|
|
611
|
-
const absPath = resolve2(file);
|
|
612
|
-
let content;
|
|
613
|
-
try {
|
|
614
|
-
content = readFileSync2(absPath, "utf-8");
|
|
615
|
-
} catch {
|
|
616
|
-
console.error(`Cannot read file: ${absPath}`);
|
|
617
|
-
return process.exit(1);
|
|
618
|
-
}
|
|
619
|
-
const result = parseAtelier(content);
|
|
620
|
-
if (!result.success) {
|
|
621
|
-
console.error("Parse errors:");
|
|
622
|
-
for (const error of result.errors) {
|
|
623
|
-
console.error(` - ${error.path}: ${error.message}`);
|
|
624
|
-
}
|
|
625
|
-
return process.exit(1);
|
|
626
|
-
}
|
|
627
|
-
return result.data;
|
|
628
|
-
}
|
|
629
|
-
function infoCommand(program) {
|
|
630
|
-
program.command("info <file>").description("Display summary info for an .atelier file").action((file) => {
|
|
631
|
-
const doc = readAndParse(file);
|
|
632
|
-
const info = getInfo(doc);
|
|
633
|
-
console.log(formatInfo(info));
|
|
634
|
-
});
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
// src/commands/still.ts
|
|
638
|
-
import { readFileSync as readFileSync3, writeFileSync } from "fs";
|
|
639
|
-
import { resolve as resolve3 } from "path";
|
|
640
|
-
function resolveStill(doc, stateName, frame) {
|
|
641
|
-
const stateNames = Object.keys(doc.states);
|
|
642
|
-
if (stateNames.length === 0) {
|
|
643
|
-
throw new Error("Document has no states");
|
|
644
|
-
}
|
|
645
|
-
const resolvedStateName = stateName ?? stateNames[0];
|
|
646
|
-
if (!(resolvedStateName in doc.states)) {
|
|
647
|
-
throw new Error(
|
|
648
|
-
`State "${resolvedStateName}" not found. Available: ${stateNames.join(", ")}`
|
|
649
|
-
);
|
|
650
|
-
}
|
|
651
|
-
const resolvedFrame = frame ?? 0;
|
|
652
|
-
return resolveFrame(doc, resolvedStateName, resolvedFrame);
|
|
653
|
-
}
|
|
654
|
-
function readAndParse2(file) {
|
|
655
|
-
const absPath = resolve3(file);
|
|
656
|
-
let content;
|
|
657
|
-
try {
|
|
658
|
-
content = readFileSync3(absPath, "utf-8");
|
|
659
|
-
} catch {
|
|
660
|
-
console.error(`Cannot read file: ${absPath}`);
|
|
661
|
-
return process.exit(1);
|
|
662
|
-
}
|
|
663
|
-
const result = parseAtelier(content);
|
|
664
|
-
if (!result.success) {
|
|
665
|
-
console.error("Parse errors:");
|
|
666
|
-
for (const error of result.errors) {
|
|
667
|
-
console.error(` - ${error.path}: ${error.message}`);
|
|
668
|
-
}
|
|
669
|
-
return process.exit(1);
|
|
670
|
-
}
|
|
671
|
-
return result.data;
|
|
672
|
-
}
|
|
673
|
-
function stillCommand(program) {
|
|
674
|
-
program.command("still <file>").description(
|
|
675
|
-
"Resolve a single frame and output as JSON or render as PNG"
|
|
676
|
-
).option("-s, --state <name>", "State name (defaults to first state)").option(
|
|
677
|
-
"-f, --frame <number>",
|
|
678
|
-
"Frame number (defaults to 0)",
|
|
679
|
-
"0"
|
|
680
|
-
).option(
|
|
681
|
-
"--format <type>",
|
|
682
|
-
"Output format: json | png (default: json)",
|
|
683
|
-
"json"
|
|
684
|
-
).option("-o, --output <path>", "Output file path (default: stdout)").action(
|
|
685
|
-
async (file, options) => {
|
|
686
|
-
const doc = readAndParse2(file);
|
|
687
|
-
const frameNumber = parseInt(options.frame, 10);
|
|
688
|
-
if (isNaN(frameNumber) || frameNumber < 0) {
|
|
689
|
-
console.error(
|
|
690
|
-
`Invalid frame number: ${options.frame}`
|
|
691
|
-
);
|
|
692
|
-
process.exit(1);
|
|
693
|
-
return;
|
|
694
|
-
}
|
|
695
|
-
if (options.format !== "json" && options.format !== "png") {
|
|
696
|
-
console.error(`Unknown format: "${options.format}". Use json or png.`);
|
|
697
|
-
process.exit(1);
|
|
698
|
-
return;
|
|
699
|
-
}
|
|
700
|
-
try {
|
|
701
|
-
const resolved = resolveStill(
|
|
702
|
-
doc,
|
|
703
|
-
options.state,
|
|
704
|
-
frameNumber
|
|
705
|
-
);
|
|
706
|
-
if (options.format === "json") {
|
|
707
|
-
const json = JSON.stringify(resolved, null, 2);
|
|
708
|
-
if (options.output) {
|
|
709
|
-
writeFileSync(resolve3(options.output), json, "utf-8");
|
|
710
|
-
} else {
|
|
711
|
-
console.log(json);
|
|
712
|
-
}
|
|
713
|
-
} else {
|
|
714
|
-
let canvasMod;
|
|
715
|
-
try {
|
|
716
|
-
canvasMod = await import("canvas");
|
|
717
|
-
} catch {
|
|
718
|
-
console.error("PNG output requires the 'canvas' package. Install it: npm i canvas");
|
|
719
|
-
process.exit(1);
|
|
720
|
-
return;
|
|
721
|
-
}
|
|
722
|
-
const { renderFrame: renderFrame2 } = await import("./dist-M67UZGFQ.js");
|
|
723
|
-
const { createCanvas } = canvasMod;
|
|
724
|
-
const cvs = createCanvas(doc.canvas.width, doc.canvas.height);
|
|
725
|
-
const ctx = cvs.getContext("2d");
|
|
726
|
-
renderFrame2(ctx, resolved, doc);
|
|
727
|
-
const buffer = cvs.toBuffer("image/png");
|
|
728
|
-
if (options.output) {
|
|
729
|
-
writeFileSync(resolve3(options.output), buffer);
|
|
730
|
-
} else {
|
|
731
|
-
process.stdout.write(buffer);
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
} catch (err) {
|
|
735
|
-
console.error(
|
|
736
|
-
err.message
|
|
737
|
-
);
|
|
738
|
-
process.exit(1);
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
);
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
// src/commands/render-pipeline.ts
|
|
745
|
-
import { spawn } from "child_process";
|
|
746
|
-
async function checkFfmpeg() {
|
|
747
|
-
return new Promise((resolve9) => {
|
|
748
|
-
const proc = spawn("ffmpeg", ["-version"], { stdio: "pipe" });
|
|
749
|
-
proc.on("error", () => resolve9(false));
|
|
750
|
-
proc.on("close", (code) => resolve9(code === 0));
|
|
751
|
-
});
|
|
752
|
-
}
|
|
753
|
-
function buildFfmpegArgs(width, height, fps, format, output) {
|
|
754
|
-
const input = [
|
|
755
|
-
"-y",
|
|
756
|
-
"-f",
|
|
757
|
-
"rawvideo",
|
|
758
|
-
"-pix_fmt",
|
|
759
|
-
"bgra",
|
|
760
|
-
"-s",
|
|
761
|
-
`${width}x${height}`,
|
|
762
|
-
"-r",
|
|
763
|
-
String(fps),
|
|
764
|
-
"-i",
|
|
765
|
-
"pipe:0"
|
|
766
|
-
];
|
|
767
|
-
if (format === "mp4") {
|
|
768
|
-
return [
|
|
769
|
-
...input,
|
|
770
|
-
"-c:v",
|
|
771
|
-
"libx264",
|
|
772
|
-
"-pix_fmt",
|
|
773
|
-
"yuv420p",
|
|
774
|
-
"-preset",
|
|
775
|
-
"medium",
|
|
776
|
-
"-crf",
|
|
777
|
-
"18",
|
|
778
|
-
"-movflags",
|
|
779
|
-
"+faststart",
|
|
780
|
-
output
|
|
781
|
-
];
|
|
782
|
-
}
|
|
783
|
-
return [
|
|
784
|
-
...input,
|
|
785
|
-
"-vf",
|
|
786
|
-
"split[s0][s1];[s0]palettegen=stats_mode=single[p];[s1][p]paletteuse=dither=sierra2_4a",
|
|
787
|
-
"-loop",
|
|
788
|
-
"0",
|
|
789
|
-
output
|
|
790
|
-
];
|
|
791
|
-
}
|
|
792
|
-
async function preloadImages(doc, loadImage) {
|
|
793
|
-
const sources = /* @__PURE__ */ new Set();
|
|
794
|
-
for (const layer of doc.layers) {
|
|
795
|
-
if (layer.visual.type === "image") {
|
|
796
|
-
const iv = layer.visual;
|
|
797
|
-
if (iv.src) {
|
|
798
|
-
sources.add(iv.src);
|
|
799
|
-
} else if (iv.assetId && doc.assets?.[iv.assetId]) {
|
|
800
|
-
sources.add(doc.assets[iv.assetId].src);
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
if (sources.size === 0) {
|
|
805
|
-
return new ImageCache();
|
|
806
|
-
}
|
|
807
|
-
const preloaded = /* @__PURE__ */ new Map();
|
|
808
|
-
await Promise.all(
|
|
809
|
-
[...sources].map(async (src) => {
|
|
810
|
-
try {
|
|
811
|
-
preloaded.set(src, await loadImage(src));
|
|
812
|
-
} catch {
|
|
813
|
-
}
|
|
814
|
-
})
|
|
815
|
-
);
|
|
816
|
-
const imageCache = new ImageCache({
|
|
817
|
-
createImage: (src, onLoad, onError) => {
|
|
818
|
-
const img = preloaded.get(src);
|
|
819
|
-
if (img) {
|
|
820
|
-
process.nextTick(onLoad);
|
|
821
|
-
return img;
|
|
822
|
-
}
|
|
823
|
-
process.nextTick(onError);
|
|
824
|
-
return {};
|
|
825
|
-
}
|
|
826
|
-
});
|
|
827
|
-
for (const src of preloaded.keys()) {
|
|
828
|
-
imageCache.load(src);
|
|
829
|
-
}
|
|
830
|
-
await new Promise((resolve9) => process.nextTick(resolve9));
|
|
831
|
-
return imageCache;
|
|
832
|
-
}
|
|
833
|
-
async function renderDocument(doc, opts) {
|
|
834
|
-
const canvasModuleName = "canvas";
|
|
835
|
-
let createCanvas;
|
|
836
|
-
let loadImage;
|
|
837
|
-
try {
|
|
838
|
-
const canvasModule = await import(
|
|
839
|
-
/* webpackIgnore: true */
|
|
840
|
-
canvasModuleName
|
|
841
|
-
);
|
|
842
|
-
createCanvas = canvasModule.createCanvas;
|
|
843
|
-
loadImage = canvasModule.loadImage;
|
|
844
|
-
} catch {
|
|
845
|
-
throw new Error(
|
|
846
|
-
"The 'canvas' package is not installed.\nInstall it with:\n npm install canvas\nPrerequisites vary by OS:\n macOS: brew install pkg-config cairo pango libpng jpeg giflib librsvg pixman\n Ubuntu: sudo apt install build-essential libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev\nSee: https://github.com/Automattic/node-canvas#compiling"
|
|
847
|
-
);
|
|
848
|
-
}
|
|
849
|
-
const { width, height, fps } = doc.canvas;
|
|
850
|
-
const { output, format, states, onProgress } = opts;
|
|
851
|
-
if (format === "mp4" && (width % 2 !== 0 || height % 2 !== 0)) {
|
|
852
|
-
throw new Error(
|
|
853
|
-
`H.264 requires even dimensions. Canvas is ${width}\xD7${height}. Try ${width + width % 2}\xD7${height + height % 2}.`
|
|
854
|
-
);
|
|
855
|
-
}
|
|
856
|
-
const allStates = Object.keys(doc.states);
|
|
857
|
-
const renderStates = states ?? allStates;
|
|
858
|
-
for (const s of renderStates) {
|
|
859
|
-
if (!(s in doc.states)) {
|
|
860
|
-
throw new Error(
|
|
861
|
-
`State "${s}" not found. Available: ${allStates.join(", ")}`
|
|
862
|
-
);
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
let totalFrames = 0;
|
|
866
|
-
for (const s of renderStates) {
|
|
867
|
-
totalFrames += doc.states[s].duration;
|
|
868
|
-
}
|
|
869
|
-
if (totalFrames === 0) {
|
|
870
|
-
throw new Error("Nothing to render \u2014 all states have duration 0");
|
|
871
|
-
}
|
|
872
|
-
const imageCache = await preloadImages(doc, loadImage);
|
|
873
|
-
const canvas = createCanvas(width, height);
|
|
874
|
-
const ctx = canvas.getContext("2d");
|
|
875
|
-
const ffmpegArgs = buildFfmpegArgs(width, height, fps, format, output);
|
|
876
|
-
const ffmpeg = spawn("ffmpeg", ffmpegArgs, {
|
|
877
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
878
|
-
});
|
|
879
|
-
let stderrOutput = "";
|
|
880
|
-
ffmpeg.stderr?.on("data", (chunk) => {
|
|
881
|
-
stderrOutput += chunk.toString();
|
|
882
|
-
});
|
|
883
|
-
const startTime = Date.now();
|
|
884
|
-
let frameIndex = 0;
|
|
885
|
-
for (const stateName of renderStates) {
|
|
886
|
-
const duration = doc.states[stateName].duration;
|
|
887
|
-
for (let f = 0; f < duration; f++) {
|
|
888
|
-
const resolved = resolveFrame(doc, stateName, f);
|
|
889
|
-
renderFrame(ctx, resolved, doc, imageCache);
|
|
890
|
-
const raw = canvas.toBuffer("raw");
|
|
891
|
-
const canWrite = ffmpeg.stdin.write(raw);
|
|
892
|
-
if (!canWrite) {
|
|
893
|
-
await new Promise(
|
|
894
|
-
(resolve9) => ffmpeg.stdin.once("drain", resolve9)
|
|
895
|
-
);
|
|
896
|
-
}
|
|
897
|
-
frameIndex++;
|
|
898
|
-
onProgress?.({
|
|
899
|
-
frame: frameIndex,
|
|
900
|
-
totalFrames,
|
|
901
|
-
state: stateName,
|
|
902
|
-
percent: Math.round(frameIndex / totalFrames * 100)
|
|
903
|
-
});
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
ffmpeg.stdin.end();
|
|
907
|
-
const exitCode = await new Promise((resolve9) => {
|
|
908
|
-
ffmpeg.on("close", resolve9);
|
|
909
|
-
});
|
|
910
|
-
if (exitCode !== 0) {
|
|
911
|
-
throw new Error(
|
|
912
|
-
`FFmpeg exited with code ${exitCode}.
|
|
913
|
-
${stderrOutput.slice(-500)}`
|
|
914
|
-
);
|
|
915
|
-
}
|
|
916
|
-
return {
|
|
917
|
-
output,
|
|
918
|
-
format,
|
|
919
|
-
totalFrames,
|
|
920
|
-
states: renderStates,
|
|
921
|
-
durationMs: Date.now() - startTime
|
|
922
|
-
};
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
// src/commands/render.ts
|
|
926
|
-
import { readFileSync as readFileSync4 } from "fs";
|
|
927
|
-
import { resolve as resolve4, basename, extname } from "path";
|
|
928
|
-
function readAndParse3(file) {
|
|
929
|
-
const absPath = resolve4(file);
|
|
930
|
-
let content;
|
|
931
|
-
try {
|
|
932
|
-
content = readFileSync4(absPath, "utf-8");
|
|
933
|
-
} catch {
|
|
934
|
-
console.error(`Cannot read file: ${absPath}`);
|
|
935
|
-
return process.exit(1);
|
|
936
|
-
}
|
|
937
|
-
const result = parseAtelier(content);
|
|
938
|
-
if (!result.success) {
|
|
939
|
-
console.error("Parse errors:");
|
|
940
|
-
for (const error of result.errors) {
|
|
941
|
-
console.error(` - ${error.path}: ${error.message}`);
|
|
942
|
-
}
|
|
943
|
-
return process.exit(1);
|
|
944
|
-
}
|
|
945
|
-
return result.data;
|
|
946
|
-
}
|
|
947
|
-
function inferFormat(output) {
|
|
948
|
-
if (!output) return "mp4";
|
|
949
|
-
const ext = extname(output).toLowerCase();
|
|
950
|
-
if (ext === ".gif") return "gif";
|
|
951
|
-
return "mp4";
|
|
952
|
-
}
|
|
953
|
-
function renderCommand(program) {
|
|
954
|
-
program.command("render <file>").description("Render animation to MP4 or GIF via FFmpeg").option("-o, --output <path>", "Output file path").option("-f, --format <type>", "Output format: mp4 | gif").option(
|
|
955
|
-
"-s, --state <names...>",
|
|
956
|
-
"State(s) to render (default: all in order)"
|
|
957
|
-
).action(
|
|
958
|
-
async (file, options) => {
|
|
959
|
-
const hasFfmpeg = await checkFfmpeg();
|
|
960
|
-
if (!hasFfmpeg) {
|
|
961
|
-
console.error("FFmpeg is not installed or not in PATH.");
|
|
962
|
-
console.error("Install it:");
|
|
963
|
-
console.error(" macOS: brew install ffmpeg");
|
|
964
|
-
console.error(" Ubuntu: sudo apt install ffmpeg");
|
|
965
|
-
console.error(" Windows: https://ffmpeg.org/download.html");
|
|
966
|
-
process.exit(1);
|
|
967
|
-
return;
|
|
968
|
-
}
|
|
969
|
-
const doc = readAndParse3(file);
|
|
970
|
-
let format;
|
|
971
|
-
if (options.format) {
|
|
972
|
-
if (options.format !== "mp4" && options.format !== "gif") {
|
|
973
|
-
console.error(
|
|
974
|
-
`Unknown format: "${options.format}". Use mp4 or gif.`
|
|
975
|
-
);
|
|
976
|
-
process.exit(1);
|
|
977
|
-
return;
|
|
978
|
-
}
|
|
979
|
-
format = options.format;
|
|
980
|
-
} else {
|
|
981
|
-
format = inferFormat(options.output);
|
|
982
|
-
}
|
|
983
|
-
const inputName = basename(file, extname(file));
|
|
984
|
-
const output = options.output ?? `${inputName}.${format}`;
|
|
985
|
-
const startTime = Date.now();
|
|
986
|
-
try {
|
|
987
|
-
const result = await renderDocument(doc, {
|
|
988
|
-
output: resolve4(output),
|
|
989
|
-
format,
|
|
990
|
-
states: options.state,
|
|
991
|
-
onProgress: ({ frame, totalFrames, state, percent }) => {
|
|
992
|
-
const elapsed = (Date.now() - startTime) / 1e3;
|
|
993
|
-
const rate = elapsed > 0 ? frame / elapsed : 0;
|
|
994
|
-
const remaining = rate > 0 ? (totalFrames - frame) / rate : 0;
|
|
995
|
-
process.stderr.write(
|
|
996
|
-
`\rRendering: frame ${frame}/${totalFrames} (${percent}%) - state "${state}" - ETA ${remaining.toFixed(1)}s`
|
|
997
|
-
);
|
|
998
|
-
}
|
|
999
|
-
});
|
|
1000
|
-
process.stderr.write("\n");
|
|
1001
|
-
console.log(
|
|
1002
|
-
`Done: ${result.totalFrames} frames \u2192 ${result.output} (${(result.durationMs / 1e3).toFixed(1)}s)`
|
|
1003
|
-
);
|
|
1004
|
-
} catch (err) {
|
|
1005
|
-
process.stderr.write("\n");
|
|
1006
|
-
console.error(err.message);
|
|
1007
|
-
process.exit(1);
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
);
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
// src/commands/export-svg.ts
|
|
1014
|
-
import { readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
|
|
1015
|
-
import { resolve as resolve5 } from "path";
|
|
1016
|
-
|
|
1017
|
-
// ../svg/dist/index.js
|
|
1018
|
-
function buildTransform(eff) {
|
|
1019
|
-
const parts = [];
|
|
1020
|
-
if (eff.x !== 0 || eff.y !== 0) {
|
|
1021
|
-
parts.push(`translate(${eff.x}, ${eff.y})`);
|
|
1022
|
-
}
|
|
1023
|
-
const totalRotation = eff.rotation + eff.motionPathAngle;
|
|
1024
|
-
if (totalRotation !== 0 || eff.scaleX !== 1 || eff.scaleY !== 1) {
|
|
1025
|
-
const ax = eff.anchorX * eff.width;
|
|
1026
|
-
const ay = eff.anchorY * eff.height;
|
|
1027
|
-
if (ax !== 0 || ay !== 0) {
|
|
1028
|
-
parts.push(`translate(${ax}, ${ay})`);
|
|
1029
|
-
}
|
|
1030
|
-
if (totalRotation !== 0) {
|
|
1031
|
-
parts.push(`rotate(${totalRotation})`);
|
|
1032
|
-
}
|
|
1033
|
-
if (eff.scaleX !== 1 || eff.scaleY !== 1) {
|
|
1034
|
-
parts.push(`scale(${eff.scaleX}, ${eff.scaleY})`);
|
|
1035
|
-
}
|
|
1036
|
-
if (ax !== 0 || ay !== 0) {
|
|
1037
|
-
parts.push(`translate(${-ax}, ${-ay})`);
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
return parts.length > 0 ? parts.join(" ") : "";
|
|
1041
|
-
}
|
|
1042
|
-
function buildStyleAttrs(eff) {
|
|
1043
|
-
const attrs = [];
|
|
1044
|
-
if (eff.opacity < 1) {
|
|
1045
|
-
attrs.push(`opacity="${eff.opacity}"`);
|
|
1046
|
-
}
|
|
1047
|
-
if (eff.blendMode !== "normal") {
|
|
1048
|
-
attrs.push(`style="mix-blend-mode: ${eff.blendMode}"`);
|
|
1049
|
-
}
|
|
1050
|
-
return attrs.join(" ");
|
|
1051
|
-
}
|
|
1052
|
-
function escapeXml(str) {
|
|
1053
|
-
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1054
|
-
}
|
|
1055
|
-
var gradientIdCounter = 0;
|
|
1056
|
-
function resetGradientCounter() {
|
|
1057
|
-
gradientIdCounter = 0;
|
|
1058
|
-
}
|
|
1059
|
-
function colorToCSS(color) {
|
|
1060
|
-
if (typeof color === "string") return color;
|
|
1061
|
-
if ("r" in color) {
|
|
1062
|
-
const c = color;
|
|
1063
|
-
return `rgba(${Math.round(c.r)}, ${Math.round(c.g)}, ${Math.round(c.b)}, ${c.a})`;
|
|
1064
|
-
}
|
|
1065
|
-
if ("h" in color) {
|
|
1066
|
-
const c = color;
|
|
1067
|
-
return `hsla(${c.h}, ${c.s}%, ${c.l}%, ${c.a})`;
|
|
1068
|
-
}
|
|
1069
|
-
return "#000000";
|
|
1070
|
-
}
|
|
1071
|
-
function buildGradientDef(fill, width, height) {
|
|
1072
|
-
if (fill.type === "solid") {
|
|
1073
|
-
return { defs: "", fillRef: colorToCSS(fill.color) };
|
|
1074
|
-
}
|
|
1075
|
-
if (fill.type === "linear-gradient") {
|
|
1076
|
-
return buildLinearGradient(fill, width, height);
|
|
1077
|
-
}
|
|
1078
|
-
if (fill.type === "radial-gradient") {
|
|
1079
|
-
return buildRadialGradient(fill, width, height);
|
|
1080
|
-
}
|
|
1081
|
-
return { defs: "", fillRef: "none" };
|
|
1082
|
-
}
|
|
1083
|
-
function buildLinearGradient(fill, _width, _height) {
|
|
1084
|
-
const id = `grad-${++gradientIdCounter}`;
|
|
1085
|
-
const rad = fill.angle * Math.PI / 180;
|
|
1086
|
-
const cos = Math.cos(rad);
|
|
1087
|
-
const sin = Math.sin(rad);
|
|
1088
|
-
const x1 = 0.5 - cos * 0.5;
|
|
1089
|
-
const y1 = 0.5 - sin * 0.5;
|
|
1090
|
-
const x2 = 0.5 + cos * 0.5;
|
|
1091
|
-
const y2 = 0.5 + sin * 0.5;
|
|
1092
|
-
const stops = fill.stops.map(
|
|
1093
|
-
(s) => `<stop offset="${s.offset}" stop-color="${colorToCSS(s.color)}" />`
|
|
1094
|
-
).join("");
|
|
1095
|
-
const def = `<linearGradient id="${id}" x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}">${stops}</linearGradient>`;
|
|
1096
|
-
return { defs: def, fillRef: `url(#${id})` };
|
|
1097
|
-
}
|
|
1098
|
-
function buildRadialGradient(fill, width, height) {
|
|
1099
|
-
const id = `grad-${++gradientIdCounter}`;
|
|
1100
|
-
const cx = typeof fill.center.x === "number" ? fill.center.x : parseFloat(fill.center.x) / 100 * width;
|
|
1101
|
-
const cy = typeof fill.center.y === "number" ? fill.center.y : parseFloat(fill.center.y) / 100 * height;
|
|
1102
|
-
const r = typeof fill.radius === "number" ? fill.radius : parseFloat(fill.radius) / 100 * Math.max(width, height);
|
|
1103
|
-
const stops = fill.stops.map(
|
|
1104
|
-
(s) => `<stop offset="${s.offset}" stop-color="${colorToCSS(s.color)}" />`
|
|
1105
|
-
).join("");
|
|
1106
|
-
const def = `<radialGradient id="${id}" cx="${cx}" cy="${cy}" r="${r}" gradientUnits="userSpaceOnUse">${stops}</radialGradient>`;
|
|
1107
|
-
return { defs: def, fillRef: `url(#${id})` };
|
|
1108
|
-
}
|
|
1109
|
-
function renderShapeSVG(eff, visual) {
|
|
1110
|
-
const { shape } = visual;
|
|
1111
|
-
const defs = [];
|
|
1112
|
-
let fillAttr = "none";
|
|
1113
|
-
if (visual.fill) {
|
|
1114
|
-
const gradResult = buildGradientDef(visual.fill, eff.width, eff.height);
|
|
1115
|
-
if (gradResult.defs) defs.push(gradResult.defs);
|
|
1116
|
-
fillAttr = gradResult.fillRef;
|
|
1117
|
-
}
|
|
1118
|
-
let strokeAttrs = "";
|
|
1119
|
-
if (visual.stroke) {
|
|
1120
|
-
strokeAttrs = buildStrokeAttrs(visual.stroke);
|
|
1121
|
-
}
|
|
1122
|
-
const element = buildShapeElement(shape, eff.width, eff.height, fillAttr, strokeAttrs);
|
|
1123
|
-
return { elements: element, defs: defs.join("") };
|
|
1124
|
-
}
|
|
1125
|
-
function buildShapeElement(shape, width, height, fill, strokeAttrs) {
|
|
1126
|
-
switch (shape.type) {
|
|
1127
|
-
case "rect":
|
|
1128
|
-
return buildRectElement(shape, width, height, fill, strokeAttrs);
|
|
1129
|
-
case "ellipse":
|
|
1130
|
-
return buildEllipseElement(width, height, fill, strokeAttrs);
|
|
1131
|
-
case "path":
|
|
1132
|
-
return buildPathElement(shape, fill, strokeAttrs);
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
function buildRectElement(shape, width, height, fill, strokeAttrs) {
|
|
1136
|
-
let rx = "";
|
|
1137
|
-
if (shape.cornerRadius) {
|
|
1138
|
-
const r = typeof shape.cornerRadius === "number" ? shape.cornerRadius : shape.cornerRadius[0];
|
|
1139
|
-
rx = ` rx="${r}" ry="${r}"`;
|
|
1140
|
-
}
|
|
1141
|
-
return `<rect width="${width}" height="${height}" fill="${fill}"${rx}${strokeAttrs ? " " + strokeAttrs : ""} />`;
|
|
1142
|
-
}
|
|
1143
|
-
function buildEllipseElement(width, height, fill, strokeAttrs) {
|
|
1144
|
-
const cx = width / 2;
|
|
1145
|
-
const cy = height / 2;
|
|
1146
|
-
const rx = width / 2;
|
|
1147
|
-
const ry = height / 2;
|
|
1148
|
-
return `<ellipse cx="${cx}" cy="${cy}" rx="${rx}" ry="${ry}" fill="${fill}"${strokeAttrs ? " " + strokeAttrs : ""} />`;
|
|
1149
|
-
}
|
|
1150
|
-
function buildPathElement(shape, fill, strokeAttrs) {
|
|
1151
|
-
if (shape.points.length < 2) return "";
|
|
1152
|
-
const d = [];
|
|
1153
|
-
d.push(`M ${shape.points[0].x} ${shape.points[0].y}`);
|
|
1154
|
-
for (let i = 1; i < shape.points.length; i++) {
|
|
1155
|
-
const prev = shape.points[i - 1];
|
|
1156
|
-
const curr = shape.points[i];
|
|
1157
|
-
if (prev.out && curr.in) {
|
|
1158
|
-
d.push(`C ${prev.x + prev.out.x} ${prev.y + prev.out.y} ${curr.x + curr.in.x} ${curr.y + curr.in.y} ${curr.x} ${curr.y}`);
|
|
1159
|
-
} else {
|
|
1160
|
-
d.push(`L ${curr.x} ${curr.y}`);
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
if (shape.closed) d.push("Z");
|
|
1164
|
-
return `<path d="${d.join(" ")}" fill="${fill}"${strokeAttrs ? " " + strokeAttrs : ""} />`;
|
|
1165
|
-
}
|
|
1166
|
-
function buildStrokeAttrs(stroke) {
|
|
1167
|
-
const parts = [];
|
|
1168
|
-
parts.push(`stroke="${colorToCSS(stroke.color)}"`);
|
|
1169
|
-
parts.push(`stroke-width="${stroke.width}"`);
|
|
1170
|
-
if (stroke.lineCap) parts.push(`stroke-linecap="${stroke.lineCap}"`);
|
|
1171
|
-
if (stroke.lineJoin) parts.push(`stroke-linejoin="${stroke.lineJoin}"`);
|
|
1172
|
-
if (stroke.dash) parts.push(`stroke-dasharray="${stroke.dash.join(" ")}"`);
|
|
1173
|
-
return parts.join(" ");
|
|
1174
|
-
}
|
|
1175
|
-
function renderTextSVG(eff, visual) {
|
|
1176
|
-
const { style } = visual;
|
|
1177
|
-
const attrs = [];
|
|
1178
|
-
attrs.push(`font-family="${escapeXml(style.fontFamily)}"`);
|
|
1179
|
-
attrs.push(`font-size="${style.fontSize}"`);
|
|
1180
|
-
if (style.fontWeight && style.fontWeight !== "normal") {
|
|
1181
|
-
attrs.push(`font-weight="${style.fontWeight}"`);
|
|
1182
|
-
}
|
|
1183
|
-
if (style.fontStyle && style.fontStyle !== "normal") {
|
|
1184
|
-
attrs.push(`font-style="${style.fontStyle}"`);
|
|
1185
|
-
}
|
|
1186
|
-
attrs.push(`fill="${colorToCSS(style.color)}"`);
|
|
1187
|
-
const align = style.textAlign ?? "left";
|
|
1188
|
-
let textAnchor = "start";
|
|
1189
|
-
let x = 0;
|
|
1190
|
-
if (align === "center") {
|
|
1191
|
-
textAnchor = "middle";
|
|
1192
|
-
x = eff.width / 2;
|
|
1193
|
-
} else if (align === "right") {
|
|
1194
|
-
textAnchor = "end";
|
|
1195
|
-
x = eff.width;
|
|
1196
|
-
}
|
|
1197
|
-
attrs.push(`text-anchor="${textAnchor}"`);
|
|
1198
|
-
if (style.letterSpacing) {
|
|
1199
|
-
attrs.push(`letter-spacing="${style.letterSpacing}"`);
|
|
1200
|
-
}
|
|
1201
|
-
attrs.push(`dominant-baseline="hanging"`);
|
|
1202
|
-
return `<text x="${x}" y="0" ${attrs.join(" ")}>${escapeXml(visual.content)}</text>`;
|
|
1203
|
-
}
|
|
1204
|
-
var filterIdCounter = 0;
|
|
1205
|
-
function resetFilterCounter() {
|
|
1206
|
-
filterIdCounter = 0;
|
|
1207
|
-
}
|
|
1208
|
-
function buildShadowFilter(eff) {
|
|
1209
|
-
if (!eff.shadow) return null;
|
|
1210
|
-
const id = `filter-${++filterIdCounter}`;
|
|
1211
|
-
const { color, blur, offsetX, offsetY } = eff.shadow;
|
|
1212
|
-
const def = [
|
|
1213
|
-
`<filter id="${id}" x="-50%" y="-50%" width="200%" height="200%">`,
|
|
1214
|
-
`<feDropShadow dx="${offsetX}" dy="${offsetY}" stdDeviation="${blur / 2}" flood-color="${color}" />`,
|
|
1215
|
-
`</filter>`
|
|
1216
|
-
].join("");
|
|
1217
|
-
return { defs: def, filterRef: `url(#${id})` };
|
|
1218
|
-
}
|
|
1219
|
-
function buildTintFilter(eff) {
|
|
1220
|
-
if (!eff.tint || eff.tint.amount <= 0) return null;
|
|
1221
|
-
const id = `filter-${++filterIdCounter}`;
|
|
1222
|
-
const { color, amount } = eff.tint;
|
|
1223
|
-
const def = [
|
|
1224
|
-
`<filter id="${id}" x="0%" y="0%" width="100%" height="100%">`,
|
|
1225
|
-
`<feFlood flood-color="${color}" flood-opacity="${amount}" result="tint" />`,
|
|
1226
|
-
`<feBlend in="SourceGraphic" in2="tint" mode="multiply" />`,
|
|
1227
|
-
`</filter>`
|
|
1228
|
-
].join("");
|
|
1229
|
-
return { defs: def, filterRef: `url(#${id})` };
|
|
1230
|
-
}
|
|
1231
|
-
var clipIdCounter = 0;
|
|
1232
|
-
function resetClipCounter() {
|
|
1233
|
-
clipIdCounter = 0;
|
|
1234
|
-
}
|
|
1235
|
-
function buildClipPathDef(shape, width, height) {
|
|
1236
|
-
const id = `clip-${++clipIdCounter}`;
|
|
1237
|
-
let pathContent = "";
|
|
1238
|
-
switch (shape.type) {
|
|
1239
|
-
case "rect":
|
|
1240
|
-
if (shape.cornerRadius) {
|
|
1241
|
-
const r = typeof shape.cornerRadius === "number" ? shape.cornerRadius : shape.cornerRadius[0];
|
|
1242
|
-
pathContent = `<rect width="${width}" height="${height}" rx="${r}" ry="${r}" />`;
|
|
1243
|
-
} else {
|
|
1244
|
-
pathContent = `<rect width="${width}" height="${height}" />`;
|
|
1245
|
-
}
|
|
1246
|
-
break;
|
|
1247
|
-
case "ellipse":
|
|
1248
|
-
pathContent = `<ellipse cx="${width / 2}" cy="${height / 2}" rx="${width / 2}" ry="${height / 2}" />`;
|
|
1249
|
-
break;
|
|
1250
|
-
case "path": {
|
|
1251
|
-
if (shape.points.length < 2) return { defs: "", clipRef: "" };
|
|
1252
|
-
const d = [];
|
|
1253
|
-
d.push(`M ${shape.points[0].x} ${shape.points[0].y}`);
|
|
1254
|
-
for (let i = 1; i < shape.points.length; i++) {
|
|
1255
|
-
const prev = shape.points[i - 1];
|
|
1256
|
-
const curr = shape.points[i];
|
|
1257
|
-
if (prev.out && curr.in) {
|
|
1258
|
-
d.push(`C ${prev.x + prev.out.x} ${prev.y + prev.out.y} ${curr.x + curr.in.x} ${curr.y + curr.in.y} ${curr.x} ${curr.y}`);
|
|
1259
|
-
} else {
|
|
1260
|
-
d.push(`L ${curr.x} ${curr.y}`);
|
|
1261
|
-
}
|
|
1262
|
-
}
|
|
1263
|
-
if (shape.closed) d.push("Z");
|
|
1264
|
-
pathContent = `<path d="${d.join(" ")}" />`;
|
|
1265
|
-
break;
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1268
|
-
const def = `<clipPath id="${id}">${pathContent}</clipPath>`;
|
|
1269
|
-
return { defs: def, clipRef: `url(#${id})` };
|
|
1270
|
-
}
|
|
1271
|
-
function renderFrameSVG(doc, stateOrFrame, frame, opts) {
|
|
1272
|
-
resetGradientCounter();
|
|
1273
|
-
resetFilterCounter();
|
|
1274
|
-
resetClipCounter();
|
|
1275
|
-
let resolved;
|
|
1276
|
-
if (typeof stateOrFrame === "string") {
|
|
1277
|
-
resolved = resolveFrame(doc, stateOrFrame, frame ?? 0);
|
|
1278
|
-
} else {
|
|
1279
|
-
resolved = stateOrFrame;
|
|
1280
|
-
}
|
|
1281
|
-
const { width, height } = doc.canvas;
|
|
1282
|
-
const indent = opts?.indent ?? 2;
|
|
1283
|
-
const pad = " ".repeat(indent);
|
|
1284
|
-
const effLayers = resolved.layers.map(
|
|
1285
|
-
(rl) => buildEffectiveLayer(rl, width, height)
|
|
1286
|
-
);
|
|
1287
|
-
const allDefs = [];
|
|
1288
|
-
const layerElements = [];
|
|
1289
|
-
for (let i = 0; i < effLayers.length; i++) {
|
|
1290
|
-
const eff = effLayers[i];
|
|
1291
|
-
const layer = resolved.layers[i].layer;
|
|
1292
|
-
if (!eff.visible) continue;
|
|
1293
|
-
if (eff.opacity <= 0) continue;
|
|
1294
|
-
if (layer.visual.type === "image") {
|
|
1295
|
-
const iv = eff.visual;
|
|
1296
|
-
if (!iv.src && iv.assetId && doc.assets?.[iv.assetId]) {
|
|
1297
|
-
eff.visual.src = doc.assets[iv.assetId].src;
|
|
1298
|
-
}
|
|
1299
|
-
}
|
|
1300
|
-
const transform = buildTransform(eff);
|
|
1301
|
-
const styleAttrs = buildStyleAttrs(eff);
|
|
1302
|
-
const filterResult = buildShadowFilter(eff);
|
|
1303
|
-
if (filterResult) allDefs.push(filterResult.defs);
|
|
1304
|
-
const tintResult = buildTintFilter(eff);
|
|
1305
|
-
if (tintResult) allDefs.push(tintResult.defs);
|
|
1306
|
-
let clipAttr = "";
|
|
1307
|
-
if (layer.clipPath) {
|
|
1308
|
-
const clipResult = buildClipPathDef(layer.clipPath, eff.width, eff.height);
|
|
1309
|
-
if (clipResult.defs) {
|
|
1310
|
-
allDefs.push(clipResult.defs);
|
|
1311
|
-
clipAttr = ` clip-path="${clipResult.clipRef}"`;
|
|
1312
|
-
}
|
|
1313
|
-
}
|
|
1314
|
-
const gAttrs = [];
|
|
1315
|
-
if (transform) gAttrs.push(`transform="${transform}"`);
|
|
1316
|
-
if (styleAttrs) gAttrs.push(styleAttrs);
|
|
1317
|
-
if (filterResult && tintResult) {
|
|
1318
|
-
gAttrs.push(`filter="${filterResult.filterRef}"`);
|
|
1319
|
-
} else if (filterResult) {
|
|
1320
|
-
gAttrs.push(`filter="${filterResult.filterRef}"`);
|
|
1321
|
-
} else if (tintResult) {
|
|
1322
|
-
gAttrs.push(`filter="${tintResult.filterRef}"`);
|
|
1323
|
-
}
|
|
1324
|
-
if (clipAttr) gAttrs.push(clipAttr.trim());
|
|
1325
|
-
let content = "";
|
|
1326
|
-
let layerDefs = "";
|
|
1327
|
-
switch (layer.visual.type) {
|
|
1328
|
-
case "shape": {
|
|
1329
|
-
const result = renderShapeSVG(eff, eff.visual);
|
|
1330
|
-
content = result.elements;
|
|
1331
|
-
layerDefs = result.defs;
|
|
1332
|
-
break;
|
|
1333
|
-
}
|
|
1334
|
-
case "text":
|
|
1335
|
-
content = renderTextSVG(eff, eff.visual);
|
|
1336
|
-
break;
|
|
1337
|
-
case "image": {
|
|
1338
|
-
const iv = eff.visual;
|
|
1339
|
-
if (iv.src) {
|
|
1340
|
-
if (iv.spritesheet) {
|
|
1341
|
-
const { columns, rows, frameWidth, frameHeight, frameCount } = iv.spritesheet;
|
|
1342
|
-
const maxFrames = frameCount ?? columns * rows;
|
|
1343
|
-
const idx = Math.max(0, Math.min(Math.floor(iv.frameIndex ?? 0), maxFrames - 1));
|
|
1344
|
-
const col = idx % columns;
|
|
1345
|
-
const row = Math.floor(idx / columns);
|
|
1346
|
-
const sx = col * frameWidth;
|
|
1347
|
-
const sy = row * frameHeight;
|
|
1348
|
-
const imgW = columns * frameWidth;
|
|
1349
|
-
const imgH = rows * frameHeight;
|
|
1350
|
-
content = `<svg viewBox="${sx} ${sy} ${frameWidth} ${frameHeight}" width="${eff.width}" height="${eff.height}"><image href="${escapeXml(iv.src)}" width="${imgW}" height="${imgH}" /></svg>`;
|
|
1351
|
-
} else if (iv.sourceRect) {
|
|
1352
|
-
const sr = iv.sourceRect;
|
|
1353
|
-
content = `<svg viewBox="${sr.x} ${sr.y} ${sr.width} ${sr.height}" width="${eff.width}" height="${eff.height}"><image href="${escapeXml(iv.src)}" width="${eff.width}" height="${eff.height}" /></svg>`;
|
|
1354
|
-
} else {
|
|
1355
|
-
content = `<image href="${escapeXml(iv.src)}" width="${eff.width}" height="${eff.height}" />`;
|
|
1356
|
-
}
|
|
1357
|
-
}
|
|
1358
|
-
break;
|
|
1359
|
-
}
|
|
1360
|
-
case "group":
|
|
1361
|
-
break;
|
|
1362
|
-
case "ref": {
|
|
1363
|
-
const refVisual = eff.visual;
|
|
1364
|
-
const refContent = renderRefSVG(eff, refVisual, doc, opts);
|
|
1365
|
-
content = refContent;
|
|
1366
|
-
break;
|
|
1367
|
-
}
|
|
1368
|
-
}
|
|
1369
|
-
if (layerDefs) allDefs.push(layerDefs);
|
|
1370
|
-
if (content) {
|
|
1371
|
-
const gOpen = gAttrs.length > 0 ? `<g ${gAttrs.join(" ")}>` : "<g>";
|
|
1372
|
-
layerElements.push(`${pad}${gOpen}${content}</g>`);
|
|
1373
|
-
}
|
|
1374
|
-
}
|
|
1375
|
-
const lines = [];
|
|
1376
|
-
if (opts?.xmlDeclaration) {
|
|
1377
|
-
lines.push('<?xml version="1.0" encoding="UTF-8"?>');
|
|
1378
|
-
}
|
|
1379
|
-
const viewBox = opts?.viewBox !== false ? ` viewBox="0 0 ${width} ${height}"` : "";
|
|
1380
|
-
const bg = doc.canvas.background;
|
|
1381
|
-
lines.push(`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}"${viewBox}>`);
|
|
1382
|
-
if (allDefs.length > 0) {
|
|
1383
|
-
lines.push(`${pad}<defs>`);
|
|
1384
|
-
for (const def of allDefs) {
|
|
1385
|
-
lines.push(`${pad}${pad}${def}`);
|
|
1386
|
-
}
|
|
1387
|
-
lines.push(`${pad}</defs>`);
|
|
1388
|
-
}
|
|
1389
|
-
if (bg && bg !== "transparent") {
|
|
1390
|
-
lines.push(`${pad}<rect width="${width}" height="${height}" fill="${bg}" />`);
|
|
1391
|
-
}
|
|
1392
|
-
lines.push(...layerElements);
|
|
1393
|
-
lines.push("</svg>");
|
|
1394
|
-
return lines.join("\n");
|
|
1395
|
-
}
|
|
1396
|
-
function renderRefSVG(eff, visual, _parentDoc, opts, _depth, _visitedRefs) {
|
|
1397
|
-
const resolver = opts?.documentResolver;
|
|
1398
|
-
if (!resolver) {
|
|
1399
|
-
return `<rect width="${eff.width}" height="${eff.height}" fill="none" stroke="#999" stroke-dasharray="4 2" />`;
|
|
1400
|
-
}
|
|
1401
|
-
const depth = _depth ?? 0;
|
|
1402
|
-
const maxDepth = opts?.maxRefDepth ?? 4;
|
|
1403
|
-
if (depth >= maxDepth) {
|
|
1404
|
-
return `<rect width="${eff.width}" height="${eff.height}" fill="none" stroke="#c33" stroke-dasharray="4 2" /><text x="${eff.width / 2}" y="${eff.height / 2}" text-anchor="middle" dominant-baseline="middle" fill="#c33" font-size="12">MAX DEPTH</text>`;
|
|
1405
|
-
}
|
|
1406
|
-
const visitedRefs = _visitedRefs ?? /* @__PURE__ */ new Set();
|
|
1407
|
-
if (visitedRefs.has(visual.src)) {
|
|
1408
|
-
return `<rect width="${eff.width}" height="${eff.height}" fill="none" stroke="#c33" stroke-dasharray="4 2" /><text x="${eff.width / 2}" y="${eff.height / 2}" text-anchor="middle" dominant-baseline="middle" fill="#c33" font-size="12">CYCLE</text>`;
|
|
1409
|
-
}
|
|
1410
|
-
const subDoc = resolver(visual.src);
|
|
1411
|
-
if (!subDoc) {
|
|
1412
|
-
return `<rect width="${eff.width}" height="${eff.height}" fill="none" stroke="#999" stroke-dasharray="4 2" /><text x="${eff.width / 2}" y="${eff.height / 2}" text-anchor="middle" dominant-baseline="middle" fill="#999" font-size="12">NOT FOUND</text>`;
|
|
1413
|
-
}
|
|
1414
|
-
const stateNames = Object.keys(subDoc.states);
|
|
1415
|
-
if (stateNames.length === 0) {
|
|
1416
|
-
return `<rect width="${eff.width}" height="${eff.height}" fill="none" stroke="#999" stroke-dasharray="4 2" />`;
|
|
1417
|
-
}
|
|
1418
|
-
const stateName = visual.state ?? stateNames[0];
|
|
1419
|
-
const stateObj = subDoc.states[stateName];
|
|
1420
|
-
if (!stateObj) {
|
|
1421
|
-
return `<rect width="${eff.width}" height="${eff.height}" fill="none" stroke="#999" stroke-dasharray="4 2" />`;
|
|
1422
|
-
}
|
|
1423
|
-
const maxFrame = Math.max(0, stateObj.duration - 1);
|
|
1424
|
-
const frame = Math.min(visual.frame ?? 0, maxFrame);
|
|
1425
|
-
visitedRefs.add(visual.src);
|
|
1426
|
-
const subW = subDoc.canvas.width;
|
|
1427
|
-
const subH = subDoc.canvas.height;
|
|
1428
|
-
const resolved = resolveFrame(subDoc, stateName, frame);
|
|
1429
|
-
const subParts = [];
|
|
1430
|
-
for (const rl of resolved.layers) {
|
|
1431
|
-
const subEff = buildEffectiveLayer(rl, subW, subH);
|
|
1432
|
-
if (!subEff.visible || subEff.opacity <= 0) continue;
|
|
1433
|
-
if (rl.layer.visual.type === "image") {
|
|
1434
|
-
const iv = subEff.visual;
|
|
1435
|
-
if (!iv.src && iv.assetId && subDoc.assets?.[iv.assetId]) {
|
|
1436
|
-
iv.src = subDoc.assets[iv.assetId].src;
|
|
1437
|
-
}
|
|
1438
|
-
}
|
|
1439
|
-
const transform = buildTransform(subEff);
|
|
1440
|
-
const styleAttrs = buildStyleAttrs(subEff);
|
|
1441
|
-
const gAttrs = [];
|
|
1442
|
-
if (transform) gAttrs.push(`transform="${transform}"`);
|
|
1443
|
-
if (styleAttrs) gAttrs.push(styleAttrs);
|
|
1444
|
-
let childContent = "";
|
|
1445
|
-
switch (rl.layer.visual.type) {
|
|
1446
|
-
case "shape": {
|
|
1447
|
-
const result = renderShapeSVG(subEff, subEff.visual);
|
|
1448
|
-
childContent = result.elements;
|
|
1449
|
-
break;
|
|
1450
|
-
}
|
|
1451
|
-
case "text":
|
|
1452
|
-
childContent = renderTextSVG(subEff, subEff.visual);
|
|
1453
|
-
break;
|
|
1454
|
-
case "image": {
|
|
1455
|
-
const iv = subEff.visual;
|
|
1456
|
-
if (iv.src) {
|
|
1457
|
-
childContent = `<image href="${escapeXml(iv.src)}" width="${subEff.width}" height="${subEff.height}" />`;
|
|
1458
|
-
}
|
|
1459
|
-
break;
|
|
1460
|
-
}
|
|
1461
|
-
case "ref": {
|
|
1462
|
-
const refV = subEff.visual;
|
|
1463
|
-
childContent = renderRefSVG(subEff, refV, subDoc, opts, depth + 1, visitedRefs);
|
|
1464
|
-
break;
|
|
1465
|
-
}
|
|
1466
|
-
}
|
|
1467
|
-
if (childContent) {
|
|
1468
|
-
const gOpen = gAttrs.length > 0 ? `<g ${gAttrs.join(" ")}>` : "<g>";
|
|
1469
|
-
subParts.push(`${gOpen}${childContent}</g>`);
|
|
1470
|
-
}
|
|
1471
|
-
}
|
|
1472
|
-
visitedRefs.delete(visual.src);
|
|
1473
|
-
return `<svg x="0" y="0" width="${eff.width}" height="${eff.height}" viewBox="0 0 ${subW} ${subH}">${subParts.join("")}</svg>`;
|
|
1474
|
-
}
|
|
1475
|
-
|
|
1476
|
-
// src/commands/export-svg.ts
|
|
1477
|
-
function readAndParse4(file) {
|
|
1478
|
-
const absPath = resolve5(file);
|
|
1479
|
-
let content;
|
|
1480
|
-
try {
|
|
1481
|
-
content = readFileSync5(absPath, "utf-8");
|
|
1482
|
-
} catch {
|
|
1483
|
-
console.error(`Cannot read file: ${absPath}`);
|
|
1484
|
-
return process.exit(1);
|
|
1485
|
-
}
|
|
1486
|
-
const result = parseAtelier(content);
|
|
1487
|
-
if (!result.success) {
|
|
1488
|
-
console.error("Parse errors:");
|
|
1489
|
-
for (const error of result.errors) {
|
|
1490
|
-
console.error(` - ${error.path}: ${error.message}`);
|
|
1491
|
-
}
|
|
1492
|
-
return process.exit(1);
|
|
1493
|
-
}
|
|
1494
|
-
return result.data;
|
|
1495
|
-
}
|
|
1496
|
-
function exportSvgCommand(program) {
|
|
1497
|
-
program.command("export-svg <file>").description("Export a frame as SVG").option("-s, --state <name>", "State name (defaults to first state)").option("-f, --frame <number>", "Frame number (defaults to 0)", "0").option("-o, --output <path>", "Output file path (default: stdout)").option("--xml-declaration", "Include XML declaration").action(
|
|
1498
|
-
(file, options) => {
|
|
1499
|
-
const doc = readAndParse4(file);
|
|
1500
|
-
const frameNumber = parseInt(options.frame, 10);
|
|
1501
|
-
if (isNaN(frameNumber) || frameNumber < 0) {
|
|
1502
|
-
console.error(`Invalid frame number: ${options.frame}`);
|
|
1503
|
-
process.exit(1);
|
|
1504
|
-
return;
|
|
1505
|
-
}
|
|
1506
|
-
const stateNames = Object.keys(doc.states);
|
|
1507
|
-
if (stateNames.length === 0) {
|
|
1508
|
-
console.error("Document has no states");
|
|
1509
|
-
process.exit(1);
|
|
1510
|
-
return;
|
|
1511
|
-
}
|
|
1512
|
-
const stateName = options.state ?? stateNames[0];
|
|
1513
|
-
if (!doc.states[stateName]) {
|
|
1514
|
-
console.error(`State "${stateName}" not found. Available: ${stateNames.join(", ")}`);
|
|
1515
|
-
process.exit(1);
|
|
1516
|
-
return;
|
|
1517
|
-
}
|
|
1518
|
-
try {
|
|
1519
|
-
const svg = renderFrameSVG(doc, stateName, frameNumber, {
|
|
1520
|
-
xmlDeclaration: options.xmlDeclaration
|
|
1521
|
-
});
|
|
1522
|
-
if (options.output) {
|
|
1523
|
-
writeFileSync2(resolve5(options.output), svg, "utf-8");
|
|
1524
|
-
} else {
|
|
1525
|
-
console.log(svg);
|
|
1526
|
-
}
|
|
1527
|
-
} catch (err) {
|
|
1528
|
-
console.error(err.message);
|
|
1529
|
-
process.exit(1);
|
|
1530
|
-
}
|
|
1531
|
-
}
|
|
1532
|
-
);
|
|
1533
|
-
}
|
|
1534
|
-
|
|
1535
|
-
// src/commands/export-lottie.ts
|
|
1536
|
-
import { readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
|
|
1537
|
-
import { resolve as resolve6 } from "path";
|
|
1538
|
-
|
|
1539
|
-
// ../lottie/dist/index.js
|
|
1540
|
-
function colorToLottie(color) {
|
|
1541
|
-
if (typeof color === "string") {
|
|
1542
|
-
return hexToLottie(color);
|
|
1543
|
-
}
|
|
1544
|
-
if ("r" in color) {
|
|
1545
|
-
const c = color;
|
|
1546
|
-
return [c.r / 255, c.g / 255, c.b / 255, c.a];
|
|
1547
|
-
}
|
|
1548
|
-
if ("h" in color) {
|
|
1549
|
-
const c = color;
|
|
1550
|
-
const rgb = hslToRgb(c.h, c.s, c.l);
|
|
1551
|
-
return [rgb[0] / 255, rgb[1] / 255, rgb[2] / 255, c.a];
|
|
1552
|
-
}
|
|
1553
|
-
return [0, 0, 0, 1];
|
|
1554
|
-
}
|
|
1555
|
-
function hexToLottie(hex) {
|
|
1556
|
-
const clean = hex.replace("#", "");
|
|
1557
|
-
if (clean.length === 3 || clean.length === 4) {
|
|
1558
|
-
const r2 = parseInt(clean[0] + clean[0], 16) / 255;
|
|
1559
|
-
const g2 = parseInt(clean[1] + clean[1], 16) / 255;
|
|
1560
|
-
const b2 = parseInt(clean[2] + clean[2], 16) / 255;
|
|
1561
|
-
const a2 = clean.length === 4 ? parseInt(clean[3] + clean[3], 16) / 255 : 1;
|
|
1562
|
-
return [r2, g2, b2, a2];
|
|
1563
|
-
}
|
|
1564
|
-
const r = parseInt(clean.slice(0, 2), 16) / 255;
|
|
1565
|
-
const g = parseInt(clean.slice(2, 4), 16) / 255;
|
|
1566
|
-
const b = parseInt(clean.slice(4, 6), 16) / 255;
|
|
1567
|
-
const a = clean.length === 8 ? parseInt(clean.slice(6, 8), 16) / 255 : 1;
|
|
1568
|
-
return [r, g, b, a];
|
|
1569
|
-
}
|
|
1570
|
-
function hslToRgb(h, s, l) {
|
|
1571
|
-
s = s / 100;
|
|
1572
|
-
l = l / 100;
|
|
1573
|
-
const a = s * Math.min(l, 1 - l);
|
|
1574
|
-
const f = (n) => {
|
|
1575
|
-
const k = (n + h / 30) % 12;
|
|
1576
|
-
return l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
|
|
1577
|
-
};
|
|
1578
|
-
return [Math.round(f(0) * 255), Math.round(f(8) * 255), Math.round(f(4) * 255)];
|
|
1579
|
-
}
|
|
1580
|
-
function mapShapeVisual(visual, width, height) {
|
|
1581
|
-
const items = [];
|
|
1582
|
-
switch (visual.shape.type) {
|
|
1583
|
-
case "rect":
|
|
1584
|
-
items.push({
|
|
1585
|
-
ty: "rc",
|
|
1586
|
-
nm: "Rectangle",
|
|
1587
|
-
d: 1,
|
|
1588
|
-
s: { a: 0, k: [width, height] },
|
|
1589
|
-
p: { a: 0, k: [width / 2, height / 2] },
|
|
1590
|
-
r: { a: 0, k: typeof visual.shape.cornerRadius === "number" ? visual.shape.cornerRadius : 0 }
|
|
1591
|
-
});
|
|
1592
|
-
break;
|
|
1593
|
-
case "ellipse":
|
|
1594
|
-
items.push({
|
|
1595
|
-
ty: "el",
|
|
1596
|
-
nm: "Ellipse",
|
|
1597
|
-
d: 1,
|
|
1598
|
-
s: { a: 0, k: [width, height] },
|
|
1599
|
-
p: { a: 0, k: [width / 2, height / 2] }
|
|
1600
|
-
});
|
|
1601
|
-
break;
|
|
1602
|
-
case "path": {
|
|
1603
|
-
const vertices = visual.shape.points.map((p) => [p.x, p.y]);
|
|
1604
|
-
const inTangents = visual.shape.points.map((p) => p.in ? [p.in.x, p.in.y] : [0, 0]);
|
|
1605
|
-
const outTangents = visual.shape.points.map((p) => p.out ? [p.out.x, p.out.y] : [0, 0]);
|
|
1606
|
-
items.push({
|
|
1607
|
-
ty: "sh",
|
|
1608
|
-
nm: "Path",
|
|
1609
|
-
ks: {
|
|
1610
|
-
a: 0,
|
|
1611
|
-
k: {
|
|
1612
|
-
c: visual.shape.closed ?? false,
|
|
1613
|
-
v: vertices,
|
|
1614
|
-
i: inTangents,
|
|
1615
|
-
o: outTangents
|
|
1616
|
-
}
|
|
1617
|
-
}
|
|
1618
|
-
});
|
|
1619
|
-
break;
|
|
1620
|
-
}
|
|
1621
|
-
}
|
|
1622
|
-
if (visual.fill) {
|
|
1623
|
-
items.push(mapFill(visual.fill, width, height));
|
|
1624
|
-
}
|
|
1625
|
-
if (visual.stroke) {
|
|
1626
|
-
items.push(mapStroke(visual.stroke));
|
|
1627
|
-
}
|
|
1628
|
-
return items;
|
|
1629
|
-
}
|
|
1630
|
-
function mapFill(fill, _width, _height) {
|
|
1631
|
-
if (fill.type === "solid") {
|
|
1632
|
-
const color = colorToLottie(fill.color);
|
|
1633
|
-
return {
|
|
1634
|
-
ty: "fl",
|
|
1635
|
-
nm: "Fill",
|
|
1636
|
-
c: { a: 0, k: color.slice(0, 3) },
|
|
1637
|
-
o: { a: 0, k: (color[3] ?? 1) * 100 },
|
|
1638
|
-
r: 1
|
|
1639
|
-
};
|
|
1640
|
-
}
|
|
1641
|
-
if (fill.type === "linear-gradient") {
|
|
1642
|
-
const stops = [];
|
|
1643
|
-
for (const stop of fill.stops) {
|
|
1644
|
-
const c = colorToLottie(stop.color);
|
|
1645
|
-
stops.push(stop.offset, c[0], c[1], c[2]);
|
|
1646
|
-
}
|
|
1647
|
-
return {
|
|
1648
|
-
ty: "gf",
|
|
1649
|
-
nm: "Gradient Fill",
|
|
1650
|
-
t: 1,
|
|
1651
|
-
// linear
|
|
1652
|
-
s: { a: 0, k: [0, 0] },
|
|
1653
|
-
e: { a: 0, k: [100, 0] },
|
|
1654
|
-
g: { p: fill.stops.length, k: { a: 0, k: stops } },
|
|
1655
|
-
r: 1,
|
|
1656
|
-
o: { a: 0, k: 100 }
|
|
1657
|
-
};
|
|
1658
|
-
}
|
|
1659
|
-
if (fill.type === "radial-gradient") {
|
|
1660
|
-
const stops = [];
|
|
1661
|
-
for (const stop of fill.stops) {
|
|
1662
|
-
const c = colorToLottie(stop.color);
|
|
1663
|
-
stops.push(stop.offset, c[0], c[1], c[2]);
|
|
1664
|
-
}
|
|
1665
|
-
return {
|
|
1666
|
-
ty: "gf",
|
|
1667
|
-
nm: "Gradient Fill",
|
|
1668
|
-
t: 2,
|
|
1669
|
-
// radial
|
|
1670
|
-
s: { a: 0, k: [50, 50] },
|
|
1671
|
-
e: { a: 0, k: [100, 50] },
|
|
1672
|
-
g: { p: fill.stops.length, k: { a: 0, k: stops } },
|
|
1673
|
-
r: 1,
|
|
1674
|
-
o: { a: 0, k: 100 }
|
|
1675
|
-
};
|
|
1676
|
-
}
|
|
1677
|
-
return { ty: "fl", nm: "Fill", c: { a: 0, k: [0, 0, 0] }, o: { a: 0, k: 100 }, r: 1 };
|
|
1678
|
-
}
|
|
1679
|
-
function mapStroke(stroke) {
|
|
1680
|
-
const color = colorToLottie(stroke.color);
|
|
1681
|
-
return {
|
|
1682
|
-
ty: "st",
|
|
1683
|
-
nm: "Stroke",
|
|
1684
|
-
c: { a: 0, k: color.slice(0, 3) },
|
|
1685
|
-
o: { a: 0, k: (color[3] ?? 1) * 100 },
|
|
1686
|
-
w: { a: 0, k: stroke.width },
|
|
1687
|
-
lc: stroke.lineCap === "round" ? 2 : stroke.lineCap === "square" ? 3 : 1,
|
|
1688
|
-
lj: stroke.lineJoin === "round" ? 2 : stroke.lineJoin === "bevel" ? 3 : 1
|
|
1689
|
-
};
|
|
1690
|
-
}
|
|
1691
|
-
function mapEasing(easing) {
|
|
1692
|
-
if (!easing) return linearEasing();
|
|
1693
|
-
if (typeof easing === "string") {
|
|
1694
|
-
switch (easing) {
|
|
1695
|
-
case "ease-in":
|
|
1696
|
-
return bezierEasing(0.42, 0, 1, 1);
|
|
1697
|
-
case "ease-out":
|
|
1698
|
-
return bezierEasing(0, 0, 0.58, 1);
|
|
1699
|
-
case "ease-in-out":
|
|
1700
|
-
return bezierEasing(0.42, 0, 0.58, 1);
|
|
1701
|
-
default:
|
|
1702
|
-
return linearEasing();
|
|
1703
|
-
}
|
|
1704
|
-
}
|
|
1705
|
-
switch (easing.type) {
|
|
1706
|
-
case "linear":
|
|
1707
|
-
return linearEasing();
|
|
1708
|
-
case "cubic-bezier":
|
|
1709
|
-
return bezierEasing(easing.x1, easing.y1, easing.x2, easing.y2);
|
|
1710
|
-
case "spring":
|
|
1711
|
-
return bezierEasing(0.25, 0.1, 0.25, 1);
|
|
1712
|
-
case "step":
|
|
1713
|
-
return { h: 1 };
|
|
1714
|
-
default:
|
|
1715
|
-
return linearEasing();
|
|
1716
|
-
}
|
|
1717
|
-
}
|
|
1718
|
-
function linearEasing() {
|
|
1719
|
-
return {
|
|
1720
|
-
i: { x: [0.833], y: [0.833] },
|
|
1721
|
-
o: { x: [0.167], y: [0.167] }
|
|
1722
|
-
};
|
|
1723
|
-
}
|
|
1724
|
-
function bezierEasing(x1, y1, x2, y2) {
|
|
1725
|
-
return {
|
|
1726
|
-
i: { x: [x2], y: [y2] },
|
|
1727
|
-
o: { x: [x1], y: [y1] }
|
|
1728
|
-
};
|
|
1729
|
-
}
|
|
1730
|
-
function isLossyEasing(easing) {
|
|
1731
|
-
if (!easing || typeof easing === "string") return null;
|
|
1732
|
-
if (easing.type === "spring") return "Spring easing approximated as cubic-bezier";
|
|
1733
|
-
if (easing.type === "step") return "Step easing mapped to hold keyframes";
|
|
1734
|
-
return null;
|
|
1735
|
-
}
|
|
1736
|
-
function mapDeltasToAnimated(deltas, property, warnings) {
|
|
1737
|
-
if (deltas.length === 0) {
|
|
1738
|
-
return { a: 0, k: 0 };
|
|
1739
|
-
}
|
|
1740
|
-
for (const d of deltas) {
|
|
1741
|
-
const lossyMsg = isLossyEasing(d.easing);
|
|
1742
|
-
if (lossyMsg) warnings.push(`${property}: ${lossyMsg}`);
|
|
1743
|
-
}
|
|
1744
|
-
for (const d of deltas) {
|
|
1745
|
-
if (isExpression(d.from) || isExpression(d.to)) {
|
|
1746
|
-
warnings.push(`${property}: Expression values not supported in Lottie, using static values`);
|
|
1747
|
-
return { a: 0, k: typeof d.from === "number" ? d.from : 0 };
|
|
1748
|
-
}
|
|
1749
|
-
}
|
|
1750
|
-
if (deltas.length === 1) {
|
|
1751
|
-
const d = deltas[0];
|
|
1752
|
-
const fromVal = resolveValue(d.from, property);
|
|
1753
|
-
const toVal = resolveValue(d.to, property);
|
|
1754
|
-
if (fromVal === toVal) {
|
|
1755
|
-
return { a: 0, k: fromVal };
|
|
1756
|
-
}
|
|
1757
|
-
const easing = mapEasing(d.easing);
|
|
1758
|
-
const kfs2 = [];
|
|
1759
|
-
if ("h" in easing) {
|
|
1760
|
-
kfs2.push({ t: d.range[0], s: [fromVal], h: 1 });
|
|
1761
|
-
kfs2.push({ t: d.range[1], s: [toVal] });
|
|
1762
|
-
} else {
|
|
1763
|
-
kfs2.push({ t: d.range[0], s: [fromVal], e: [toVal], i: easing.i, o: easing.o });
|
|
1764
|
-
kfs2.push({ t: d.range[1], s: [toVal] });
|
|
1765
|
-
}
|
|
1766
|
-
return { a: 1, k: kfs2 };
|
|
1767
|
-
}
|
|
1768
|
-
const sorted = [...deltas].sort((a, b) => a.range[0] - b.range[0]);
|
|
1769
|
-
const kfs = [];
|
|
1770
|
-
for (const d of sorted) {
|
|
1771
|
-
const fromVal = resolveValue(d.from, property);
|
|
1772
|
-
const toVal = resolveValue(d.to, property);
|
|
1773
|
-
const easing = mapEasing(d.easing);
|
|
1774
|
-
if ("h" in easing) {
|
|
1775
|
-
kfs.push({ t: d.range[0], s: [fromVal], h: 1 });
|
|
1776
|
-
} else {
|
|
1777
|
-
kfs.push({ t: d.range[0], s: [fromVal], e: [toVal], i: easing.i, o: easing.o });
|
|
1778
|
-
}
|
|
1779
|
-
}
|
|
1780
|
-
const last = sorted[sorted.length - 1];
|
|
1781
|
-
kfs.push({ t: last.range[1], s: [resolveValue(last.to, property)] });
|
|
1782
|
-
return { a: 1, k: kfs };
|
|
1783
|
-
}
|
|
1784
|
-
function mapPositionDeltas(xDeltas, yDeltas, baseX, baseY, warnings) {
|
|
1785
|
-
const hasXAnim = xDeltas.length > 0;
|
|
1786
|
-
const hasYAnim = yDeltas.length > 0;
|
|
1787
|
-
if (!hasXAnim && !hasYAnim) {
|
|
1788
|
-
return { a: 0, k: [baseX, baseY, 0] };
|
|
1789
|
-
}
|
|
1790
|
-
const frames = /* @__PURE__ */ new Set();
|
|
1791
|
-
for (const d of [...xDeltas, ...yDeltas]) {
|
|
1792
|
-
frames.add(d.range[0]);
|
|
1793
|
-
frames.add(d.range[1]);
|
|
1794
|
-
}
|
|
1795
|
-
const sortedFrames = [...frames].sort((a, b) => a - b);
|
|
1796
|
-
if (sortedFrames.length < 2) {
|
|
1797
|
-
return { a: 0, k: [baseX, baseY, 0] };
|
|
1798
|
-
}
|
|
1799
|
-
const kfs = [];
|
|
1800
|
-
for (let i = 0; i < sortedFrames.length; i++) {
|
|
1801
|
-
const f = sortedFrames[i];
|
|
1802
|
-
const x = resolveAtFrame(xDeltas, f, baseX);
|
|
1803
|
-
const y = resolveAtFrame(yDeltas, f, baseY);
|
|
1804
|
-
if (i < sortedFrames.length - 1) {
|
|
1805
|
-
const nextF = sortedFrames[i + 1];
|
|
1806
|
-
const nextX = resolveAtFrame(xDeltas, nextF, baseX);
|
|
1807
|
-
const nextY = resolveAtFrame(yDeltas, nextF, baseY);
|
|
1808
|
-
const activeX = xDeltas.find((d) => d.range[0] <= f && d.range[1] >= f);
|
|
1809
|
-
const activeY = yDeltas.find((d) => d.range[0] <= f && d.range[1] >= f);
|
|
1810
|
-
const easing = mapEasing(activeX?.easing ?? activeY?.easing);
|
|
1811
|
-
if ("h" in easing) {
|
|
1812
|
-
kfs.push({ t: f, s: [x, y, 0], h: 1 });
|
|
1813
|
-
} else {
|
|
1814
|
-
kfs.push({ t: f, s: [x, y, 0], e: [nextX, nextY, 0], i: easing.i, o: easing.o });
|
|
1815
|
-
}
|
|
1816
|
-
} else {
|
|
1817
|
-
kfs.push({ t: f, s: [x, y, 0] });
|
|
1818
|
-
}
|
|
1819
|
-
}
|
|
1820
|
-
for (const d of [...xDeltas, ...yDeltas]) {
|
|
1821
|
-
const msg = isLossyEasing(d.easing);
|
|
1822
|
-
if (msg) warnings.push(`position: ${msg}`);
|
|
1823
|
-
}
|
|
1824
|
-
return { a: 1, k: kfs };
|
|
1825
|
-
}
|
|
1826
|
-
function resolveAtFrame(deltas, frame, base) {
|
|
1827
|
-
for (const d of deltas) {
|
|
1828
|
-
if (frame >= d.range[0] && frame <= d.range[1]) {
|
|
1829
|
-
const from = typeof d.from === "number" ? d.from : base;
|
|
1830
|
-
const to = typeof d.to === "number" ? d.to : base;
|
|
1831
|
-
const progress = d.range[0] === d.range[1] ? 1 : (frame - d.range[0]) / (d.range[1] - d.range[0]);
|
|
1832
|
-
return from + (to - from) * progress;
|
|
1833
|
-
}
|
|
1834
|
-
}
|
|
1835
|
-
let lastCompleted;
|
|
1836
|
-
for (const d of deltas) {
|
|
1837
|
-
if (frame > d.range[1]) {
|
|
1838
|
-
if (!lastCompleted || d.range[1] > lastCompleted.range[1]) {
|
|
1839
|
-
lastCompleted = d;
|
|
1840
|
-
}
|
|
1841
|
-
}
|
|
1842
|
-
}
|
|
1843
|
-
if (lastCompleted) return typeof lastCompleted.to === "number" ? lastCompleted.to : base;
|
|
1844
|
-
return base;
|
|
1845
|
-
}
|
|
1846
|
-
function resolveValue(val, _property) {
|
|
1847
|
-
if (typeof val === "number") return val;
|
|
1848
|
-
if (typeof val === "string" && val.startsWith("#")) {
|
|
1849
|
-
return 0;
|
|
1850
|
-
}
|
|
1851
|
-
return 0;
|
|
1852
|
-
}
|
|
1853
|
-
function isExpression(val) {
|
|
1854
|
-
return typeof val === "object" && val !== null && "expr" in val;
|
|
1855
|
-
}
|
|
1856
|
-
function toNum(v) {
|
|
1857
|
-
return typeof v === "number" ? v : 0;
|
|
1858
|
-
}
|
|
1859
|
-
function mapLayers(doc, state, warnings) {
|
|
1860
|
-
const layerIndexMap = /* @__PURE__ */ new Map();
|
|
1861
|
-
doc.layers.forEach((l, i) => layerIndexMap.set(l.id, i));
|
|
1862
|
-
return doc.layers.map((layer, index) => {
|
|
1863
|
-
const deltas = state.deltas.filter((d) => d.layer === layer.id);
|
|
1864
|
-
return mapLayer(layer, index, deltas, layerIndexMap, doc, warnings);
|
|
1865
|
-
});
|
|
1866
|
-
}
|
|
1867
|
-
function mapLayer(layer, index, deltas, layerIndexMap, doc, warnings) {
|
|
1868
|
-
const duration = getMaxFrame(deltas, doc);
|
|
1869
|
-
const base = {
|
|
1870
|
-
ty: getLayerType(layer),
|
|
1871
|
-
nm: layer.id,
|
|
1872
|
-
ind: index,
|
|
1873
|
-
ip: 0,
|
|
1874
|
-
op: duration,
|
|
1875
|
-
st: 0,
|
|
1876
|
-
ks: buildTransform2(layer, deltas, warnings)
|
|
1877
|
-
};
|
|
1878
|
-
if (layer.parentId) {
|
|
1879
|
-
const parentIdx = layerIndexMap.get(layer.parentId);
|
|
1880
|
-
if (parentIdx !== void 0) {
|
|
1881
|
-
base.parent = parentIdx;
|
|
1882
|
-
}
|
|
1883
|
-
}
|
|
1884
|
-
if (layer.blendMode) {
|
|
1885
|
-
base.bm = mapBlendMode(layer.blendMode);
|
|
1886
|
-
}
|
|
1887
|
-
if (layer.visual.type === "shape") {
|
|
1888
|
-
base.shapes = mapShapeVisual(layer.visual, toNum(layer.bounds.width), toNum(layer.bounds.height));
|
|
1889
|
-
}
|
|
1890
|
-
if (layer.visual.type === "text") {
|
|
1891
|
-
const color = colorToLottie(layer.visual.style.color);
|
|
1892
|
-
base.t = {
|
|
1893
|
-
d: {
|
|
1894
|
-
k: [{
|
|
1895
|
-
s: {
|
|
1896
|
-
s: layer.visual.style.fontSize,
|
|
1897
|
-
f: layer.visual.style.fontFamily,
|
|
1898
|
-
t: layer.visual.content,
|
|
1899
|
-
fc: color.slice(0, 3),
|
|
1900
|
-
j: layer.visual.style.textAlign === "center" ? 1 : layer.visual.style.textAlign === "right" ? 2 : 0
|
|
1901
|
-
},
|
|
1902
|
-
t: 0
|
|
1903
|
-
}]
|
|
1904
|
-
}
|
|
1905
|
-
};
|
|
1906
|
-
}
|
|
1907
|
-
if (layer.visual.type === "image") {
|
|
1908
|
-
base.refId = layer.visual.assetId;
|
|
1909
|
-
base.w = toNum(layer.bounds.width);
|
|
1910
|
-
base.h = toNum(layer.bounds.height);
|
|
1911
|
-
if (layer.visual.spritesheet) {
|
|
1912
|
-
warnings.push(`Layer "${layer.id}": Spritesheet animation not supported in Lottie export`);
|
|
1913
|
-
}
|
|
1914
|
-
if (layer.visual.sourceRect) {
|
|
1915
|
-
warnings.push(`Layer "${layer.id}": sourceRect cropping not supported in Lottie export`);
|
|
1916
|
-
}
|
|
1917
|
-
}
|
|
1918
|
-
if (layer.tint && layer.tint.amount > 0) {
|
|
1919
|
-
warnings.push(`Layer "${layer.id}": Tint effect not supported in Lottie export`);
|
|
1920
|
-
}
|
|
1921
|
-
return base;
|
|
1922
|
-
}
|
|
1923
|
-
function getLayerType(layer) {
|
|
1924
|
-
switch (layer.visual.type) {
|
|
1925
|
-
case "shape":
|
|
1926
|
-
return 4;
|
|
1927
|
-
case "text":
|
|
1928
|
-
return 5;
|
|
1929
|
-
case "image":
|
|
1930
|
-
return 2;
|
|
1931
|
-
case "group":
|
|
1932
|
-
return 3;
|
|
1933
|
-
// null layer
|
|
1934
|
-
case "ref":
|
|
1935
|
-
return 0;
|
|
1936
|
-
// precomp
|
|
1937
|
-
default:
|
|
1938
|
-
return 4;
|
|
1939
|
-
}
|
|
1940
|
-
}
|
|
1941
|
-
function buildTransform2(layer, deltas, warnings) {
|
|
1942
|
-
const byProp = /* @__PURE__ */ new Map();
|
|
1943
|
-
for (const d of deltas) {
|
|
1944
|
-
if (!byProp.has(d.property)) byProp.set(d.property, []);
|
|
1945
|
-
byProp.get(d.property).push(d);
|
|
1946
|
-
}
|
|
1947
|
-
const xDeltas = byProp.get("frame.x") ?? [];
|
|
1948
|
-
const yDeltas = byProp.get("frame.y") ?? [];
|
|
1949
|
-
const opacityDeltas = byProp.get("opacity") ?? [];
|
|
1950
|
-
const rotationDeltas = byProp.get("rotation") ?? [];
|
|
1951
|
-
const scaleXDeltas = byProp.get("scale.x") ?? [];
|
|
1952
|
-
const scaleYDeltas = byProp.get("scale.y") ?? [];
|
|
1953
|
-
const position = mapPositionDeltas(
|
|
1954
|
-
xDeltas,
|
|
1955
|
-
yDeltas,
|
|
1956
|
-
typeof layer.frame.x === "number" ? layer.frame.x : 0,
|
|
1957
|
-
typeof layer.frame.y === "number" ? layer.frame.y : 0,
|
|
1958
|
-
warnings
|
|
1959
|
-
);
|
|
1960
|
-
let opacity;
|
|
1961
|
-
if (opacityDeltas.length > 0) {
|
|
1962
|
-
const raw = mapDeltasToAnimated(opacityDeltas, "opacity", warnings);
|
|
1963
|
-
if (raw.a === 0) {
|
|
1964
|
-
opacity = { a: 0, k: raw.k * 100 };
|
|
1965
|
-
} else {
|
|
1966
|
-
const kfs = raw.k.map((kf) => ({
|
|
1967
|
-
...kf,
|
|
1968
|
-
s: [kf.s[0] * 100],
|
|
1969
|
-
e: kf.e ? [kf.e[0] * 100] : void 0
|
|
1970
|
-
}));
|
|
1971
|
-
opacity = { a: 1, k: kfs };
|
|
1972
|
-
}
|
|
1973
|
-
} else {
|
|
1974
|
-
opacity = { a: 0, k: (layer.opacity ?? 1) * 100 };
|
|
1975
|
-
}
|
|
1976
|
-
const rotation = rotationDeltas.length > 0 ? mapDeltasToAnimated(rotationDeltas, "rotation", warnings) : { a: 0, k: layer.rotation ?? 0 };
|
|
1977
|
-
const baseScaleX = (layer.scale?.x ?? 1) * 100;
|
|
1978
|
-
const baseScaleY = (layer.scale?.y ?? 1) * 100;
|
|
1979
|
-
let scale;
|
|
1980
|
-
if (scaleXDeltas.length > 0 || scaleYDeltas.length > 0) {
|
|
1981
|
-
scale = { a: 0, k: [baseScaleX, baseScaleY, 100] };
|
|
1982
|
-
if (scaleXDeltas.length > 0 || scaleYDeltas.length > 0) {
|
|
1983
|
-
warnings.push("Scale animation partially mapped to Lottie");
|
|
1984
|
-
}
|
|
1985
|
-
} else {
|
|
1986
|
-
scale = { a: 0, k: [baseScaleX, baseScaleY, 100] };
|
|
1987
|
-
}
|
|
1988
|
-
const ax = (layer.anchorPoint?.x ?? 0) * toNum(layer.bounds.width);
|
|
1989
|
-
const ay = (layer.anchorPoint?.y ?? 0) * toNum(layer.bounds.height);
|
|
1990
|
-
return {
|
|
1991
|
-
o: opacity,
|
|
1992
|
-
r: rotation,
|
|
1993
|
-
p: position,
|
|
1994
|
-
a: { a: 0, k: [ax, ay, 0] },
|
|
1995
|
-
s: scale
|
|
1996
|
-
};
|
|
1997
|
-
}
|
|
1998
|
-
function getMaxFrame(deltas, doc) {
|
|
1999
|
-
let max = 0;
|
|
2000
|
-
for (const state of Object.values(doc.states)) {
|
|
2001
|
-
if (state.duration > max) max = state.duration;
|
|
2002
|
-
}
|
|
2003
|
-
for (const d of deltas) {
|
|
2004
|
-
if (d.range[1] > max) max = d.range[1];
|
|
2005
|
-
}
|
|
2006
|
-
return max || 60;
|
|
2007
|
-
}
|
|
2008
|
-
function mapBlendMode(mode) {
|
|
2009
|
-
const map = {
|
|
2010
|
-
normal: 0,
|
|
2011
|
-
multiply: 1,
|
|
2012
|
-
screen: 2,
|
|
2013
|
-
overlay: 3,
|
|
2014
|
-
darken: 4,
|
|
2015
|
-
lighten: 5,
|
|
2016
|
-
"color-dodge": 6,
|
|
2017
|
-
"color-burn": 7,
|
|
2018
|
-
"hard-light": 8,
|
|
2019
|
-
"soft-light": 9,
|
|
2020
|
-
difference: 10,
|
|
2021
|
-
exclusion: 11,
|
|
2022
|
-
hue: 12,
|
|
2023
|
-
saturation: 13,
|
|
2024
|
-
color: 14,
|
|
2025
|
-
luminosity: 15
|
|
2026
|
-
};
|
|
2027
|
-
return map[mode] ?? 0;
|
|
2028
|
-
}
|
|
2029
|
-
function collectUnsupportedWarnings(doc, state) {
|
|
2030
|
-
const warnings = [];
|
|
2031
|
-
if (state.audio) {
|
|
2032
|
-
warnings.push("Audio tracks are not supported in Lottie format and will be dropped");
|
|
2033
|
-
}
|
|
2034
|
-
for (const delta of state.deltas) {
|
|
2035
|
-
if (isExpression2(delta.from) || isExpression2(delta.to)) {
|
|
2036
|
-
warnings.push(`Expression values on "${delta.layer}.${delta.property}" not supported in Lottie`);
|
|
2037
|
-
}
|
|
2038
|
-
}
|
|
2039
|
-
for (const layer of doc.layers) {
|
|
2040
|
-
if (layer.shadow) {
|
|
2041
|
-
warnings.push(`Shadow on layer "${layer.id}" is not supported in base Lottie format`);
|
|
2042
|
-
}
|
|
2043
|
-
}
|
|
2044
|
-
for (const layer of doc.layers) {
|
|
2045
|
-
if (layer.motionPath) {
|
|
2046
|
-
warnings.push(`Motion path on layer "${layer.id}" is not directly mappable to Lottie`);
|
|
2047
|
-
}
|
|
2048
|
-
}
|
|
2049
|
-
for (const layer of doc.layers) {
|
|
2050
|
-
if (layer.clipPath) {
|
|
2051
|
-
warnings.push(`Clip path on layer "${layer.id}" mapped as Lottie mask (partial support)`);
|
|
2052
|
-
}
|
|
2053
|
-
}
|
|
2054
|
-
if (Object.keys(doc.states).length > 1) {
|
|
2055
|
-
warnings.push("Multiple states flattened to single timeline in Lottie export");
|
|
2056
|
-
}
|
|
2057
|
-
return warnings;
|
|
2058
|
-
}
|
|
2059
|
-
function isExpression2(val) {
|
|
2060
|
-
return typeof val === "object" && val !== null && "expr" in val;
|
|
2061
|
-
}
|
|
2062
|
-
function exportToLottie(doc, opts) {
|
|
2063
|
-
const stateNames = Object.keys(doc.states);
|
|
2064
|
-
if (stateNames.length === 0) {
|
|
2065
|
-
throw new Error("Document has no states to export");
|
|
2066
|
-
}
|
|
2067
|
-
const stateName = opts?.state ?? stateNames[0];
|
|
2068
|
-
const state = doc.states[stateName];
|
|
2069
|
-
if (!state) {
|
|
2070
|
-
throw new Error(`State "${stateName}" not found`);
|
|
2071
|
-
}
|
|
2072
|
-
const warnings = [];
|
|
2073
|
-
warnings.push(...collectUnsupportedWarnings(doc, state));
|
|
2074
|
-
const layers = mapLayers(doc, state, warnings);
|
|
2075
|
-
const assets = [];
|
|
2076
|
-
if (doc.assets) {
|
|
2077
|
-
for (const [id, asset] of Object.entries(doc.assets)) {
|
|
2078
|
-
if (asset.type === "image") {
|
|
2079
|
-
assets.push({
|
|
2080
|
-
id,
|
|
2081
|
-
w: 100,
|
|
2082
|
-
h: 100,
|
|
2083
|
-
p: asset.src,
|
|
2084
|
-
e: 0
|
|
2085
|
-
});
|
|
2086
|
-
}
|
|
2087
|
-
}
|
|
2088
|
-
}
|
|
2089
|
-
const json = {
|
|
2090
|
-
v: "5.7.4",
|
|
2091
|
-
fr: doc.canvas.fps,
|
|
2092
|
-
ip: 0,
|
|
2093
|
-
op: state.duration,
|
|
2094
|
-
w: doc.canvas.width,
|
|
2095
|
-
h: doc.canvas.height,
|
|
2096
|
-
nm: doc.name,
|
|
2097
|
-
layers,
|
|
2098
|
-
...assets.length > 0 ? { assets } : {}
|
|
2099
|
-
};
|
|
2100
|
-
const uniqueWarnings = [...new Set(warnings)];
|
|
2101
|
-
return { json, warnings: uniqueWarnings };
|
|
2102
|
-
}
|
|
2103
|
-
|
|
2104
|
-
// src/commands/export-lottie.ts
|
|
2105
|
-
function readAndParse5(file) {
|
|
2106
|
-
const absPath = resolve6(file);
|
|
2107
|
-
let content;
|
|
2108
|
-
try {
|
|
2109
|
-
content = readFileSync6(absPath, "utf-8");
|
|
2110
|
-
} catch {
|
|
2111
|
-
console.error(`Cannot read file: ${absPath}`);
|
|
2112
|
-
return process.exit(1);
|
|
2113
|
-
}
|
|
2114
|
-
const result = parseAtelier(content);
|
|
2115
|
-
if (!result.success) {
|
|
2116
|
-
console.error("Parse errors:");
|
|
2117
|
-
for (const error of result.errors) {
|
|
2118
|
-
console.error(` - ${error.path}: ${error.message}`);
|
|
2119
|
-
}
|
|
2120
|
-
return process.exit(1);
|
|
2121
|
-
}
|
|
2122
|
-
return result.data;
|
|
2123
|
-
}
|
|
2124
|
-
function exportLottieCommand(program) {
|
|
2125
|
-
program.command("export-lottie <file>").description("Export a document to Lottie JSON format").option("-s, --state <name>", "State name (defaults to first state)").option("-o, --output <path>", "Output file path (default: stdout)").action(
|
|
2126
|
-
(file, options) => {
|
|
2127
|
-
const doc = readAndParse5(file);
|
|
2128
|
-
try {
|
|
2129
|
-
const { json, warnings } = exportToLottie(doc, {
|
|
2130
|
-
state: options.state
|
|
2131
|
-
});
|
|
2132
|
-
for (const warning of warnings) {
|
|
2133
|
-
console.error(`Warning: ${warning}`);
|
|
2134
|
-
}
|
|
2135
|
-
const output = JSON.stringify(json, null, 2);
|
|
2136
|
-
if (options.output) {
|
|
2137
|
-
writeFileSync3(resolve6(options.output), output, "utf-8");
|
|
2138
|
-
} else {
|
|
2139
|
-
console.log(output);
|
|
2140
|
-
}
|
|
2141
|
-
} catch (err) {
|
|
2142
|
-
console.error(err.message);
|
|
2143
|
-
process.exit(1);
|
|
2144
|
-
}
|
|
2145
|
-
}
|
|
2146
|
-
);
|
|
2147
|
-
}
|
|
2148
|
-
|
|
2149
|
-
// src/commands/assets.ts
|
|
2150
|
-
import { readFileSync as readFileSync7 } from "fs";
|
|
2151
|
-
import { resolve as resolve7 } from "path";
|
|
2152
|
-
function getAssets(doc) {
|
|
2153
|
-
const assets = doc.assets ?? {};
|
|
2154
|
-
return Object.entries(assets).map(([assetId, asset]) => {
|
|
2155
|
-
const usedByLayers = doc.layers.filter((l) => l.visual.type === "image" && l.visual.assetId === assetId).map((l) => l.id);
|
|
2156
|
-
const usedByStates = Object.entries(doc.states).filter(([, state]) => state.audio?.src === assetId).map(([name]) => name);
|
|
2157
|
-
return {
|
|
2158
|
-
assetId,
|
|
2159
|
-
type: asset.type,
|
|
2160
|
-
src: asset.src,
|
|
2161
|
-
description: asset.description,
|
|
2162
|
-
usedByLayers,
|
|
2163
|
-
usedByStates
|
|
2164
|
-
};
|
|
2165
|
-
});
|
|
2166
|
-
}
|
|
2167
|
-
function formatAssets(assets) {
|
|
2168
|
-
if (assets.length === 0) return "No assets registered.";
|
|
2169
|
-
const lines = [`Assets: ${assets.length}`];
|
|
2170
|
-
for (const a of assets) {
|
|
2171
|
-
const desc = a.description ? ` \u2014 ${a.description}` : "";
|
|
2172
|
-
lines.push(` - ${a.assetId} (${a.type}): ${a.src}${desc}`);
|
|
2173
|
-
if (a.usedByLayers.length > 0) {
|
|
2174
|
-
lines.push(` Layers: ${a.usedByLayers.join(", ")}`);
|
|
2175
|
-
}
|
|
2176
|
-
if (a.usedByStates.length > 0) {
|
|
2177
|
-
lines.push(` States: ${a.usedByStates.join(", ")}`);
|
|
2178
|
-
}
|
|
2179
|
-
}
|
|
2180
|
-
return lines.join("\n");
|
|
2181
|
-
}
|
|
2182
|
-
function readAndParse6(file) {
|
|
2183
|
-
const absPath = resolve7(file);
|
|
2184
|
-
let content;
|
|
2185
|
-
try {
|
|
2186
|
-
content = readFileSync7(absPath, "utf-8");
|
|
2187
|
-
} catch {
|
|
2188
|
-
console.error(`Cannot read file: ${absPath}`);
|
|
2189
|
-
return process.exit(1);
|
|
2190
|
-
}
|
|
2191
|
-
const result = parseAtelier(content);
|
|
2192
|
-
if (!result.success) {
|
|
2193
|
-
console.error("Parse errors:");
|
|
2194
|
-
for (const error of result.errors) {
|
|
2195
|
-
console.error(` - ${error.path}: ${error.message}`);
|
|
2196
|
-
}
|
|
2197
|
-
return process.exit(1);
|
|
2198
|
-
}
|
|
2199
|
-
return result.data;
|
|
2200
|
-
}
|
|
2201
|
-
function assetsCommand(program) {
|
|
2202
|
-
program.command("assets <file>").description("List all assets in an .atelier file with usage info").action((file) => {
|
|
2203
|
-
const doc = readAndParse6(file);
|
|
2204
|
-
const assets = getAssets(doc);
|
|
2205
|
-
console.log(formatAssets(assets));
|
|
2206
|
-
});
|
|
2207
|
-
}
|
|
2208
|
-
|
|
2209
|
-
// src/commands/variables.ts
|
|
2210
|
-
import { readFileSync as readFileSync8 } from "fs";
|
|
2211
|
-
import { resolve as resolve8 } from "path";
|
|
2212
|
-
function getVariables(doc) {
|
|
2213
|
-
const variables = doc.variables ?? {};
|
|
2214
|
-
const referenced = findTemplateVariables(doc);
|
|
2215
|
-
const entries = Object.entries(variables).map(([name, variable]) => ({
|
|
2216
|
-
name,
|
|
2217
|
-
type: variable.type,
|
|
2218
|
-
description: variable.description,
|
|
2219
|
-
default: variable.default,
|
|
2220
|
-
referenced: referenced.includes(name)
|
|
2221
|
-
}));
|
|
2222
|
-
const undeclared = referenced.filter((r) => !variables[r]);
|
|
2223
|
-
return { variables: entries, undeclared };
|
|
2224
|
-
}
|
|
2225
|
-
function formatVariables(info) {
|
|
2226
|
-
if (info.variables.length === 0 && info.undeclared.length === 0) return "No variables declared or referenced.";
|
|
2227
|
-
const lines = [];
|
|
2228
|
-
if (info.variables.length > 0) {
|
|
2229
|
-
lines.push(`Variables: ${info.variables.length}`);
|
|
2230
|
-
for (const v of info.variables) {
|
|
2231
|
-
const desc = v.description ? ` \u2014 ${v.description}` : "";
|
|
2232
|
-
const def = v.default !== void 0 ? ` [default: ${JSON.stringify(v.default)}]` : "";
|
|
2233
|
-
const ref = v.referenced ? "" : " (unused)";
|
|
2234
|
-
lines.push(` - {{${v.name}}} (${v.type})${def}${desc}${ref}`);
|
|
2235
|
-
}
|
|
2236
|
-
}
|
|
2237
|
-
if (info.undeclared.length > 0) {
|
|
2238
|
-
lines.push(`Undeclared references: ${info.undeclared.length}`);
|
|
2239
|
-
for (const name of info.undeclared) {
|
|
2240
|
-
lines.push(` - {{${name}}} (not declared in variables)`);
|
|
2241
|
-
}
|
|
2242
|
-
}
|
|
2243
|
-
return lines.join("\n");
|
|
2244
|
-
}
|
|
2245
|
-
function readAndParse7(file) {
|
|
2246
|
-
const absPath = resolve8(file);
|
|
2247
|
-
let content;
|
|
2248
|
-
try {
|
|
2249
|
-
content = readFileSync8(absPath, "utf-8");
|
|
2250
|
-
} catch {
|
|
2251
|
-
console.error(`Cannot read file: ${absPath}`);
|
|
2252
|
-
return process.exit(1);
|
|
2253
|
-
}
|
|
2254
|
-
const result = parseAtelier(content);
|
|
2255
|
-
if (!result.success) {
|
|
2256
|
-
console.error("Parse errors:");
|
|
2257
|
-
for (const error of result.errors) {
|
|
2258
|
-
console.error(` - ${error.path}: ${error.message}`);
|
|
2259
|
-
}
|
|
2260
|
-
return process.exit(1);
|
|
2261
|
-
}
|
|
2262
|
-
return result.data;
|
|
2263
|
-
}
|
|
2264
|
-
function variablesCommand(program) {
|
|
2265
|
-
program.command("variables <file>").description("List all variables in an .atelier file with usage info").action((file) => {
|
|
2266
|
-
const doc = readAndParse7(file);
|
|
2267
|
-
const info = getVariables(doc);
|
|
2268
|
-
console.log(formatVariables(info));
|
|
2269
|
-
});
|
|
2270
|
-
}
|
|
2271
|
-
|
|
2272
|
-
export {
|
|
2273
|
-
validateVideoLayer,
|
|
2274
|
-
parseAtelier,
|
|
2275
|
-
validateFile,
|
|
2276
|
-
validateCommand,
|
|
2277
|
-
getInfo,
|
|
2278
|
-
infoCommand,
|
|
2279
|
-
resolveStill,
|
|
2280
|
-
stillCommand,
|
|
2281
|
-
checkFfmpeg,
|
|
2282
|
-
buildFfmpegArgs,
|
|
2283
|
-
renderDocument,
|
|
2284
|
-
renderCommand,
|
|
2285
|
-
exportSvgCommand,
|
|
2286
|
-
exportLottieCommand,
|
|
2287
|
-
getAssets,
|
|
2288
|
-
assetsCommand,
|
|
2289
|
-
getVariables,
|
|
2290
|
-
variablesCommand
|
|
2291
|
-
};
|
|
2292
|
-
//# sourceMappingURL=chunk-JV7RGETS.js.map
|