@a-company/atelier 0.1.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.
@@ -0,0 +1,1465 @@
1
+ // src/commands/validate.ts
2
+ import { readFileSync } from "fs";
3
+ import { resolve } from "path";
4
+
5
+ // ../schema/dist/index.js
6
+ import { z } from "zod";
7
+ import { z as z2 } from "zod";
8
+ import { z as z3 } from "zod";
9
+ import { z as z4 } from "zod";
10
+ import { z as z5 } from "zod";
11
+ import { z as z6 } from "zod";
12
+ import { z as z7 } from "zod";
13
+ import { z as z8 } from "zod";
14
+ import { z as z9 } from "zod";
15
+ import { z as z10 } from "zod";
16
+ import { z as z11 } from "zod";
17
+ import { z as z12 } from "zod";
18
+ import { z as z13 } from "zod";
19
+ import { parse as yamlParse, stringify as yamlStringify } from "yaml";
20
+ var PixelSchema = z.number();
21
+ var PercentageSchema = z.string().regex(/^-?\d+(\.\d+)?%$/, {
22
+ message: 'Percentage must be a number followed by %, e.g. "50%"'
23
+ });
24
+ var UnitValueSchema = z.union([PixelSchema, PercentageSchema]);
25
+ var FrameSchema = z2.object({
26
+ x: UnitValueSchema,
27
+ y: UnitValueSchema
28
+ });
29
+ var BoundsSchema = z2.object({
30
+ width: UnitValueSchema,
31
+ height: UnitValueSchema
32
+ });
33
+ var AnchorPointSchema = z2.object({
34
+ x: z2.number().min(0).max(1),
35
+ y: z2.number().min(0).max(1)
36
+ });
37
+ var RGBAColorSchema = z3.object({
38
+ r: z3.number().min(0).max(255),
39
+ g: z3.number().min(0).max(255),
40
+ b: z3.number().min(0).max(255),
41
+ a: z3.number().min(0).max(1)
42
+ });
43
+ var HSLAColorSchema = z3.object({
44
+ h: z3.number().min(0).max(360),
45
+ s: z3.number().min(0).max(100),
46
+ l: z3.number().min(0).max(100),
47
+ a: z3.number().min(0).max(1)
48
+ });
49
+ var HexColorSchema = z3.string().regex(/^#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/, {
50
+ message: "Color must be a hex string: #RGB, #RGBA, #RRGGBB, or #RRGGBBAA"
51
+ });
52
+ var ColorSchema = z3.union([RGBAColorSchema, HSLAColorSchema, HexColorSchema]);
53
+ var PathPointSchema = z4.object({
54
+ x: z4.number(),
55
+ y: z4.number(),
56
+ in: z4.object({ x: z4.number(), y: z4.number() }).optional(),
57
+ out: z4.object({ x: z4.number(), y: z4.number() }).optional()
58
+ });
59
+ var RectShapeSchema = z4.object({
60
+ type: z4.literal("rect"),
61
+ cornerRadius: z4.union([
62
+ z4.number().min(0),
63
+ z4.tuple([z4.number().min(0), z4.number().min(0), z4.number().min(0), z4.number().min(0)])
64
+ ]).optional()
65
+ });
66
+ var EllipseShapeSchema = z4.object({
67
+ type: z4.literal("ellipse")
68
+ });
69
+ var PathShapeSchema = z4.object({
70
+ type: z4.literal("path"),
71
+ points: z4.array(PathPointSchema).min(2, "Path must have at least 2 points"),
72
+ closed: z4.boolean().optional()
73
+ });
74
+ var ShapeSchema = z4.discriminatedUnion("type", [
75
+ RectShapeSchema,
76
+ EllipseShapeSchema,
77
+ PathShapeSchema
78
+ ]);
79
+ var GradientStopSchema = z4.object({
80
+ offset: z4.number().min(0).max(1),
81
+ color: ColorSchema
82
+ });
83
+ var SolidFillSchema = z4.object({
84
+ type: z4.literal("solid"),
85
+ color: ColorSchema
86
+ });
87
+ var LinearGradientFillSchema = z4.object({
88
+ type: z4.literal("linear-gradient"),
89
+ angle: z4.number(),
90
+ stops: z4.array(GradientStopSchema).min(2, "Gradient needs at least 2 stops")
91
+ });
92
+ var RadialGradientFillSchema = z4.object({
93
+ type: z4.literal("radial-gradient"),
94
+ center: z4.object({ x: UnitValueSchema, y: UnitValueSchema }),
95
+ radius: UnitValueSchema,
96
+ stops: z4.array(GradientStopSchema).min(2, "Gradient needs at least 2 stops")
97
+ });
98
+ var FillSchema = z4.discriminatedUnion("type", [
99
+ SolidFillSchema,
100
+ LinearGradientFillSchema,
101
+ RadialGradientFillSchema
102
+ ]);
103
+ var StrokeSchema = z4.object({
104
+ color: ColorSchema,
105
+ width: z4.number().min(0),
106
+ dash: z4.array(z4.number().min(0)).optional(),
107
+ lineCap: z4.enum(["butt", "round", "square"]).optional(),
108
+ lineJoin: z4.enum(["miter", "round", "bevel"]).optional(),
109
+ strokeStart: z4.number().min(0).max(1).optional(),
110
+ strokeEnd: z4.number().min(0).max(1).optional()
111
+ });
112
+ var TextStyleSchema = z4.object({
113
+ fontFamily: z4.string().min(1, "fontFamily is required"),
114
+ fontSize: z4.number().positive("fontSize must be positive"),
115
+ fontWeight: z4.union([z4.number(), z4.enum(["normal", "bold"])]).optional(),
116
+ fontStyle: z4.enum(["normal", "italic"]).optional(),
117
+ textAlign: z4.enum(["left", "center", "right"]).optional(),
118
+ lineHeight: z4.number().positive().optional(),
119
+ letterSpacing: z4.number().optional(),
120
+ color: ColorSchema
121
+ });
122
+ var LinearEasingSchema = z5.object({ type: z5.literal("linear") });
123
+ var CubicBezierEasingSchema = z5.object({
124
+ type: z5.literal("cubic-bezier"),
125
+ x1: z5.number().min(0).max(1),
126
+ y1: z5.number(),
127
+ x2: z5.number().min(0).max(1),
128
+ y2: z5.number()
129
+ });
130
+ var SpringEasingSchema = z5.object({
131
+ type: z5.literal("spring"),
132
+ mass: z5.number().positive().optional(),
133
+ stiffness: z5.number().positive().optional(),
134
+ damping: z5.number().positive().optional(),
135
+ velocity: z5.number().optional()
136
+ });
137
+ var StepEasingSchema = z5.object({
138
+ type: z5.literal("step"),
139
+ steps: z5.number().int().positive(),
140
+ position: z5.enum(["start", "end"]).optional()
141
+ });
142
+ var EasingPresetSchema = z5.enum(["ease-in", "ease-out", "ease-in-out"]);
143
+ var EasingSchema = z5.union([
144
+ LinearEasingSchema,
145
+ CubicBezierEasingSchema,
146
+ SpringEasingSchema,
147
+ StepEasingSchema,
148
+ EasingPresetSchema
149
+ ]);
150
+ var ShadowSchema = z6.object({
151
+ color: ColorSchema,
152
+ blur: z6.number().min(0),
153
+ offsetX: z6.number().optional(),
154
+ offsetY: z6.number().optional()
155
+ });
156
+ var ShapeVisualSchema = z7.object({
157
+ type: z7.literal("shape"),
158
+ shape: ShapeSchema,
159
+ fill: FillSchema.optional(),
160
+ stroke: StrokeSchema.optional()
161
+ });
162
+ var TextVisualSchema = z7.object({
163
+ type: z7.literal("text"),
164
+ content: z7.string(),
165
+ style: TextStyleSchema
166
+ });
167
+ var ImageVisualSchema = z7.object({
168
+ type: z7.literal("image"),
169
+ assetId: z7.string().min(1, "assetId is required"),
170
+ src: z7.string().optional()
171
+ });
172
+ var GroupVisualSchema = z7.object({
173
+ type: z7.literal("group")
174
+ });
175
+ var RefVisualSchema = z7.object({
176
+ type: z7.literal("ref"),
177
+ src: z7.string().min(1, "src is required")
178
+ });
179
+ var VisualSchema = z7.discriminatedUnion("type", [
180
+ ShapeVisualSchema,
181
+ TextVisualSchema,
182
+ ImageVisualSchema,
183
+ GroupVisualSchema,
184
+ RefVisualSchema
185
+ ]);
186
+ var LayerSchema = z7.object({
187
+ id: z7.string().min(1, "Layer id is required"),
188
+ description: z7.string().optional(),
189
+ tags: z7.array(z7.string()).optional(),
190
+ visual: VisualSchema,
191
+ frame: FrameSchema,
192
+ bounds: BoundsSchema,
193
+ anchorPoint: AnchorPointSchema.optional(),
194
+ parentId: z7.string().optional(),
195
+ opacity: z7.number().min(0).max(1).optional(),
196
+ rotation: z7.number().optional(),
197
+ scale: z7.object({ x: z7.number(), y: z7.number() }).optional(),
198
+ visible: z7.boolean().optional(),
199
+ shadow: ShadowSchema.optional()
200
+ });
201
+ var AnimatablePropertySchema = z8.enum([
202
+ "frame.x",
203
+ "frame.y",
204
+ "bounds.width",
205
+ "bounds.height",
206
+ "opacity",
207
+ "rotation",
208
+ "scale.x",
209
+ "scale.y",
210
+ "anchorPoint.x",
211
+ "anchorPoint.y",
212
+ "visual.shape.cornerRadius",
213
+ "visual.fill.color",
214
+ "visual.stroke.color",
215
+ "visual.stroke.width",
216
+ "visual.stroke.start",
217
+ "visual.stroke.end",
218
+ "visual.style.fontSize",
219
+ "visual.style.color",
220
+ "shadow.color",
221
+ "shadow.blur",
222
+ "shadow.offsetX",
223
+ "shadow.offsetY"
224
+ ]);
225
+ var FrameRangeSchema = z8.tuple([
226
+ z8.number().int().min(0, "Frame start must be >= 0"),
227
+ z8.number().int().min(0, "Frame end must be >= 0")
228
+ ]).refine(([start, end]) => end >= start, {
229
+ message: "Frame range end must be >= start"
230
+ });
231
+ var DeltaSchema = z8.object({
232
+ id: z8.string().optional(),
233
+ name: z8.string().optional(),
234
+ layer: z8.string().min(1, "Delta must reference a layer id"),
235
+ property: AnimatablePropertySchema,
236
+ range: FrameRangeSchema,
237
+ from: z8.unknown(),
238
+ to: z8.unknown(),
239
+ easing: EasingSchema.optional(),
240
+ description: z8.string().optional(),
241
+ tags: z8.array(z8.string()).optional()
242
+ });
243
+ var StateSchema = z9.object({
244
+ description: z9.string().optional(),
245
+ tags: z9.array(z9.string()).optional(),
246
+ duration: z9.number().int().positive("State duration must be a positive integer (frames)"),
247
+ deltas: z9.array(DeltaSchema)
248
+ });
249
+ var PresetDeltaSchema = z10.object({
250
+ property: AnimatablePropertySchema,
251
+ offset: z10.tuple([z10.number().int().min(0), z10.number().int().min(0)]).optional(),
252
+ from: z10.unknown(),
253
+ to: z10.unknown(),
254
+ easing: EasingSchema.optional()
255
+ });
256
+ var PresetSchema = z10.object({
257
+ description: z10.string().optional(),
258
+ tags: z10.array(z10.string()).optional(),
259
+ deltas: z10.array(PresetDeltaSchema).min(1, "Preset must have at least one delta")
260
+ });
261
+ var VariableTypeSchema = z11.enum(["string", "number", "color", "asset", "boolean"]);
262
+ var VariableSchema = z11.object({
263
+ type: VariableTypeSchema,
264
+ default: z11.unknown().optional(),
265
+ description: z11.string().optional()
266
+ });
267
+ var AssetTypeSchema = z12.enum(["image", "svg", "font", "animation"]);
268
+ var AssetSchema = z12.object({
269
+ type: AssetTypeSchema,
270
+ src: z12.string().min(1, "Asset src is required"),
271
+ description: z12.string().optional()
272
+ });
273
+ var CanvasSchema = z13.object({
274
+ width: z13.number().int().positive("Canvas width must be a positive integer"),
275
+ height: z13.number().int().positive("Canvas height must be a positive integer"),
276
+ fps: z13.number().int().positive("FPS must be a positive integer"),
277
+ background: z13.string().optional()
278
+ });
279
+ var AtelierDocumentSchema = z13.object({
280
+ version: z13.string().min(1, "Version is required"),
281
+ name: z13.string().min(1, "Animation name is required"),
282
+ description: z13.string().optional(),
283
+ tags: z13.array(z13.string()).optional(),
284
+ canvas: CanvasSchema,
285
+ variables: z13.record(z13.string(), VariableSchema).optional(),
286
+ assets: z13.record(z13.string(), AssetSchema).optional(),
287
+ presets: z13.record(z13.string(), PresetSchema).optional(),
288
+ layers: z13.array(LayerSchema),
289
+ states: z13.record(z13.string(), StateSchema)
290
+ });
291
+ function formatErrors(error) {
292
+ return error.issues.map((issue) => ({
293
+ path: issue.path.join(".") || "(root)",
294
+ message: issue.message
295
+ }));
296
+ }
297
+ function validateDocument(input) {
298
+ const result = AtelierDocumentSchema.safeParse(input);
299
+ if (result.success) {
300
+ return { success: true, data: result.data };
301
+ }
302
+ return { success: false, errors: formatErrors(result.error) };
303
+ }
304
+ function parseAtelier(yamlString) {
305
+ let parsed;
306
+ try {
307
+ parsed = yamlParse(yamlString);
308
+ } catch (err) {
309
+ return {
310
+ success: false,
311
+ errors: [{ path: "(yaml)", message: `YAML parse error: ${err.message}` }]
312
+ };
313
+ }
314
+ return validateDocument(parsed);
315
+ }
316
+
317
+ // ../math/dist/index.js
318
+ function linear(t) {
319
+ return t;
320
+ }
321
+ function cubicBezier(x1, y1, x2, y2) {
322
+ return (t) => {
323
+ if (t <= 0) return 0;
324
+ if (t >= 1) return 1;
325
+ let lo = 0;
326
+ let hi = 1;
327
+ let mid = 0;
328
+ for (let i = 0; i < 20; i++) {
329
+ mid = (lo + hi) / 2;
330
+ const x = sampleBezier(x1, x2, mid);
331
+ if (Math.abs(x - t) < 1e-6) break;
332
+ if (x < t) lo = mid;
333
+ else hi = mid;
334
+ }
335
+ mid = (lo + hi) / 2;
336
+ return sampleBezier(y1, y2, mid);
337
+ };
338
+ }
339
+ function sampleBezier(p1, p2, t) {
340
+ return 3 * (1 - t) * (1 - t) * t * p1 + 3 * (1 - t) * t * t * p2 + t * t * t;
341
+ }
342
+ var easeIn = cubicBezier(0.42, 0, 1, 1);
343
+ var easeOut = cubicBezier(0, 0, 0.58, 1);
344
+ var easeInOut = cubicBezier(0.42, 0, 0.58, 1);
345
+ function step(steps, position = "end") {
346
+ return (t) => {
347
+ if (t <= 0) return position === "start" ? 1 / steps : 0;
348
+ if (t >= 1) return 1;
349
+ const s = Math.floor(t * steps);
350
+ if (position === "start") {
351
+ return Math.min((s + 1) / steps, 1);
352
+ }
353
+ return s / steps;
354
+ };
355
+ }
356
+ function spring(config = {}) {
357
+ const {
358
+ mass = 1,
359
+ stiffness = 100,
360
+ damping = 10,
361
+ velocity = 0
362
+ } = config;
363
+ const w0 = Math.sqrt(stiffness / mass);
364
+ const zeta = damping / (2 * Math.sqrt(stiffness * mass));
365
+ const duration = estimateSettleTime(zeta, w0);
366
+ return (t) => {
367
+ if (t <= 0) return 0;
368
+ if (t >= 1) return 1;
369
+ const time = t * duration;
370
+ let value;
371
+ if (zeta < 1) {
372
+ const wd = w0 * Math.sqrt(1 - zeta * zeta);
373
+ const A = 1;
374
+ const B = (zeta * w0 + velocity) / wd;
375
+ value = 1 - Math.exp(-zeta * w0 * time) * (A * Math.cos(wd * time) + B * Math.sin(wd * time));
376
+ } else if (zeta === 1) {
377
+ value = 1 - Math.exp(-w0 * time) * (1 + (w0 + velocity) * time);
378
+ } else {
379
+ const s1 = -w0 * (zeta - Math.sqrt(zeta * zeta - 1));
380
+ const s2 = -w0 * (zeta + Math.sqrt(zeta * zeta - 1));
381
+ const A = (velocity - s2) / (s1 - s2);
382
+ const B = 1 - A;
383
+ value = 1 - A * Math.exp(s1 * time) - B * Math.exp(s2 * time);
384
+ }
385
+ return value;
386
+ };
387
+ }
388
+ function estimateSettleTime(zeta, w0) {
389
+ if (zeta >= 1) {
390
+ return 10 / (zeta * w0);
391
+ }
392
+ return Math.log(1e3) / (zeta * w0);
393
+ }
394
+ function lerp(a, b, t) {
395
+ return a + (b - a) * t;
396
+ }
397
+ function clamp(value, min, max) {
398
+ return Math.min(Math.max(value, min), max);
399
+ }
400
+ function hexToRgba(hex) {
401
+ let h = hex.replace("#", "");
402
+ if (h.length === 3)
403
+ h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2] + "ff";
404
+ else if (h.length === 4)
405
+ h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2] + h[3] + h[3];
406
+ else if (h.length === 6) h = h + "ff";
407
+ return {
408
+ r: parseInt(h.slice(0, 2), 16),
409
+ g: parseInt(h.slice(2, 4), 16),
410
+ b: parseInt(h.slice(4, 6), 16),
411
+ a: parseInt(h.slice(6, 8), 16) / 255
412
+ };
413
+ }
414
+ function rgbaToHex(color) {
415
+ const r = clamp(Math.round(color.r), 0, 255).toString(16).padStart(2, "0");
416
+ const g = clamp(Math.round(color.g), 0, 255).toString(16).padStart(2, "0");
417
+ const b = clamp(Math.round(color.b), 0, 255).toString(16).padStart(2, "0");
418
+ const a = clamp(Math.round(color.a * 255), 0, 255).toString(16).padStart(2, "0");
419
+ return `#${r}${g}${b}${a === "ff" ? "" : a}`;
420
+ }
421
+ function lerpRgba(a, b, t) {
422
+ return {
423
+ r: lerp(a.r, b.r, t),
424
+ g: lerp(a.g, b.g, t),
425
+ b: lerp(a.b, b.b, t),
426
+ a: lerp(a.a, b.a, t)
427
+ };
428
+ }
429
+
430
+ // ../core/dist/index.js
431
+ function resolveEasing(easing) {
432
+ if (!easing) return linear;
433
+ if (typeof easing === "string") {
434
+ switch (easing) {
435
+ case "ease-in":
436
+ return easeIn;
437
+ case "ease-out":
438
+ return easeOut;
439
+ case "ease-in-out":
440
+ return easeInOut;
441
+ default:
442
+ return linear;
443
+ }
444
+ }
445
+ switch (easing.type) {
446
+ case "linear":
447
+ return linear;
448
+ case "cubic-bezier":
449
+ return cubicBezier(easing.x1, easing.y1, easing.x2, easing.y2);
450
+ case "spring":
451
+ return spring({
452
+ mass: easing.mass,
453
+ stiffness: easing.stiffness,
454
+ damping: easing.damping,
455
+ velocity: easing.velocity
456
+ });
457
+ case "step":
458
+ return step(easing.steps, easing.position);
459
+ default:
460
+ return linear;
461
+ }
462
+ }
463
+ function isFrameInRange(frame, range) {
464
+ return frame >= range[0] && frame <= range[1];
465
+ }
466
+ function computeProgress(frame, range) {
467
+ const [start, end] = range;
468
+ if (start === end) return 1;
469
+ return clamp((frame - start) / (end - start), 0, 1);
470
+ }
471
+ function resolveDeltaValue(delta, frame) {
472
+ if (!isFrameInRange(frame, delta.range)) {
473
+ return void 0;
474
+ }
475
+ const progress = computeProgress(frame, delta.range);
476
+ const easingFn = resolveEasing(delta.easing);
477
+ const easedProgress = easingFn(progress);
478
+ return interpolateValue(delta.from, delta.to, easedProgress);
479
+ }
480
+ function interpolateValue(from, to, t) {
481
+ if (typeof from === "number" && typeof to === "number") {
482
+ return lerp(from, to, t);
483
+ }
484
+ if (typeof from === "string" && typeof to === "string") {
485
+ if (from.startsWith("#") && to.startsWith("#")) {
486
+ return rgbaToHex(lerpRgba(hexToRgba(from), hexToRgba(to), t));
487
+ }
488
+ return t >= 1 ? to : from;
489
+ }
490
+ return t >= 1 ? to : from;
491
+ }
492
+ function resolvePropertyAtFrame(deltas, frame) {
493
+ for (const delta of deltas) {
494
+ if (isFrameInRange(frame, delta.range)) {
495
+ return resolveDeltaValue(delta, frame);
496
+ }
497
+ }
498
+ let lastCompleted;
499
+ for (const delta of deltas) {
500
+ if (frame > delta.range[1]) {
501
+ if (!lastCompleted || delta.range[1] > lastCompleted.range[1]) {
502
+ lastCompleted = delta;
503
+ }
504
+ }
505
+ }
506
+ return lastCompleted?.to;
507
+ }
508
+ function resolveFrame(doc, stateName, frame) {
509
+ const state = doc.states[stateName];
510
+ if (!state) {
511
+ throw new Error(`State "${stateName}" not found in document "${doc.name}"`);
512
+ }
513
+ const deltasByLayerProperty = groupDeltas(state.deltas);
514
+ const resolvedLayers = doc.layers.map((layer) => {
515
+ const computedProperties = {};
516
+ const layerDeltas = deltasByLayerProperty.get(layer.id);
517
+ if (layerDeltas) {
518
+ for (const [property, deltas] of layerDeltas) {
519
+ const value = resolvePropertyAtFrame(deltas, frame);
520
+ if (value !== void 0) {
521
+ computedProperties[property] = value;
522
+ }
523
+ }
524
+ }
525
+ return { id: layer.id, layer, computedProperties };
526
+ });
527
+ return { frame, stateName, layers: resolvedLayers };
528
+ }
529
+ function groupDeltas(deltas) {
530
+ const map = /* @__PURE__ */ new Map();
531
+ for (const delta of deltas) {
532
+ let layerMap = map.get(delta.layer);
533
+ if (!layerMap) {
534
+ layerMap = /* @__PURE__ */ new Map();
535
+ map.set(delta.layer, layerMap);
536
+ }
537
+ let propDeltas = layerMap.get(delta.property);
538
+ if (!propDeltas) {
539
+ propDeltas = [];
540
+ layerMap.set(delta.property, propDeltas);
541
+ }
542
+ propDeltas.push(delta);
543
+ }
544
+ return map;
545
+ }
546
+ function rangesOverlap(a, b) {
547
+ return a[0] <= b[1] && b[0] <= a[1];
548
+ }
549
+ function validateAllDeltas(deltas) {
550
+ const errors = [];
551
+ for (let i = 0; i < deltas.length; i++) {
552
+ for (let j = i + 1; j < deltas.length; j++) {
553
+ const a = deltas[i];
554
+ const b = deltas[j];
555
+ if (a.layer === b.layer && a.property === b.property && rangesOverlap(a.range, b.range)) {
556
+ errors.push({
557
+ layerId: a.layer,
558
+ property: a.property,
559
+ existingRange: a.range,
560
+ newRange: b.range,
561
+ message: `Overlapping deltas on layer "${a.layer}" property "${a.property}": [${a.range[0]}-${a.range[1]}] overlaps with [${b.range[0]}-${b.range[1]}]`
562
+ });
563
+ }
564
+ }
565
+ }
566
+ return errors;
567
+ }
568
+
569
+ // src/commands/validate.ts
570
+ function validateFile(filePath) {
571
+ const absPath = resolve(filePath);
572
+ let content;
573
+ try {
574
+ content = readFileSync(absPath, "utf-8");
575
+ } catch {
576
+ return { valid: false, errors: [`Cannot read file: ${absPath}`] };
577
+ }
578
+ const result = parseAtelier(content);
579
+ if (!result.success) {
580
+ return {
581
+ valid: false,
582
+ errors: result.errors.map(
583
+ (e) => `${e.path}: ${e.message}`
584
+ )
585
+ };
586
+ }
587
+ const overlapErrors = [];
588
+ for (const [stateName, state] of Object.entries(result.data.states)) {
589
+ const overlaps = validateAllDeltas(state.deltas);
590
+ for (const overlap of overlaps) {
591
+ overlapErrors.push(`State "${stateName}": ${overlap.message}`);
592
+ }
593
+ }
594
+ if (overlapErrors.length > 0) {
595
+ return { valid: false, errors: overlapErrors };
596
+ }
597
+ return { valid: true, errors: [] };
598
+ }
599
+ function validateCommand(program) {
600
+ program.command("validate <file>").description("Validate an .atelier YAML file").action((file) => {
601
+ const { valid, errors } = validateFile(file);
602
+ if (valid) {
603
+ console.log("Valid");
604
+ } else {
605
+ console.error("Validation errors:");
606
+ for (const error of errors) {
607
+ console.error(` - ${error}`);
608
+ }
609
+ process.exit(1);
610
+ }
611
+ });
612
+ }
613
+
614
+ // src/commands/info.ts
615
+ import { readFileSync as readFileSync2 } from "fs";
616
+ import { resolve as resolve2 } from "path";
617
+ function getInfo(doc) {
618
+ return {
619
+ name: doc.name,
620
+ description: doc.description,
621
+ canvas: {
622
+ width: doc.canvas.width,
623
+ height: doc.canvas.height,
624
+ fps: doc.canvas.fps,
625
+ background: doc.canvas.background
626
+ },
627
+ layers: {
628
+ count: doc.layers.length,
629
+ items: doc.layers.map((layer) => ({
630
+ id: layer.id,
631
+ type: layer.visual.type
632
+ }))
633
+ },
634
+ states: {
635
+ count: Object.keys(doc.states).length,
636
+ items: Object.entries(doc.states).map(([name, state]) => ({
637
+ name,
638
+ duration: state.duration,
639
+ deltaCount: state.deltas.length
640
+ }))
641
+ },
642
+ presets: {
643
+ count: doc.presets ? Object.keys(doc.presets).length : 0
644
+ }
645
+ };
646
+ }
647
+ function formatInfo(info) {
648
+ const lines = [];
649
+ lines.push(`Name: ${info.name}`);
650
+ if (info.description) {
651
+ lines.push(`Description: ${info.description}`);
652
+ }
653
+ const bg = info.canvas.background ? `, background: ${info.canvas.background}` : "";
654
+ lines.push(
655
+ `Canvas: ${info.canvas.width}x${info.canvas.height} @ ${info.canvas.fps}fps${bg}`
656
+ );
657
+ lines.push(`Layers: ${info.layers.count}`);
658
+ for (const layer of info.layers.items) {
659
+ lines.push(` - ${layer.id} (${layer.type})`);
660
+ }
661
+ lines.push(`States: ${info.states.count}`);
662
+ for (const state of info.states.items) {
663
+ lines.push(
664
+ ` - ${state.name}: ${state.duration} frames, ${state.deltaCount} deltas`
665
+ );
666
+ }
667
+ if (info.presets.count > 0) {
668
+ lines.push(`Presets: ${info.presets.count}`);
669
+ }
670
+ return lines.join("\n");
671
+ }
672
+ function readAndParse(file) {
673
+ const absPath = resolve2(file);
674
+ let content;
675
+ try {
676
+ content = readFileSync2(absPath, "utf-8");
677
+ } catch {
678
+ console.error(`Cannot read file: ${absPath}`);
679
+ return process.exit(1);
680
+ }
681
+ const result = parseAtelier(content);
682
+ if (!result.success) {
683
+ console.error("Parse errors:");
684
+ for (const error of result.errors) {
685
+ console.error(` - ${error.path}: ${error.message}`);
686
+ }
687
+ return process.exit(1);
688
+ }
689
+ return result.data;
690
+ }
691
+ function infoCommand(program) {
692
+ program.command("info <file>").description("Display summary info for an .atelier file").action((file) => {
693
+ const doc = readAndParse(file);
694
+ const info = getInfo(doc);
695
+ console.log(formatInfo(info));
696
+ });
697
+ }
698
+
699
+ // src/commands/still.ts
700
+ import { readFileSync as readFileSync3 } from "fs";
701
+ import { resolve as resolve3 } from "path";
702
+ function resolveStill(doc, stateName, frame) {
703
+ const stateNames = Object.keys(doc.states);
704
+ if (stateNames.length === 0) {
705
+ throw new Error("Document has no states");
706
+ }
707
+ const resolvedStateName = stateName ?? stateNames[0];
708
+ if (!(resolvedStateName in doc.states)) {
709
+ throw new Error(
710
+ `State "${resolvedStateName}" not found. Available: ${stateNames.join(", ")}`
711
+ );
712
+ }
713
+ const resolvedFrame = frame ?? 0;
714
+ return resolveFrame(doc, resolvedStateName, resolvedFrame);
715
+ }
716
+ function readAndParse2(file) {
717
+ const absPath = resolve3(file);
718
+ let content;
719
+ try {
720
+ content = readFileSync3(absPath, "utf-8");
721
+ } catch {
722
+ console.error(`Cannot read file: ${absPath}`);
723
+ return process.exit(1);
724
+ }
725
+ const result = parseAtelier(content);
726
+ if (!result.success) {
727
+ console.error("Parse errors:");
728
+ for (const error of result.errors) {
729
+ console.error(` - ${error.path}: ${error.message}`);
730
+ }
731
+ return process.exit(1);
732
+ }
733
+ return result.data;
734
+ }
735
+ function stillCommand(program) {
736
+ program.command("still <file>").description(
737
+ "Resolve a single frame and output as JSON"
738
+ ).option("-s, --state <name>", "State name (defaults to first state)").option(
739
+ "-f, --frame <number>",
740
+ "Frame number (defaults to 0)",
741
+ "0"
742
+ ).action(
743
+ (file, options) => {
744
+ const doc = readAndParse2(file);
745
+ const frameNumber = parseInt(options.frame, 10);
746
+ if (isNaN(frameNumber) || frameNumber < 0) {
747
+ console.error(
748
+ `Invalid frame number: ${options.frame}`
749
+ );
750
+ process.exit(1);
751
+ return;
752
+ }
753
+ try {
754
+ const resolved = resolveStill(
755
+ doc,
756
+ options.state,
757
+ frameNumber
758
+ );
759
+ console.log(JSON.stringify(resolved, null, 2));
760
+ } catch (err) {
761
+ console.error(
762
+ err.message
763
+ );
764
+ process.exit(1);
765
+ }
766
+ }
767
+ );
768
+ }
769
+
770
+ // src/commands/render-pipeline.ts
771
+ import { spawn } from "child_process";
772
+
773
+ // ../canvas/dist/index.js
774
+ function colorToCSS(color) {
775
+ if (typeof color === "string") return color;
776
+ if ("r" in color) {
777
+ const c = color;
778
+ return `rgba(${Math.round(c.r)}, ${Math.round(c.g)}, ${Math.round(c.b)}, ${c.a})`;
779
+ }
780
+ if ("h" in color) {
781
+ const c = color;
782
+ return `hsla(${c.h}, ${c.s}%, ${c.l}%, ${c.a})`;
783
+ }
784
+ return "#000000";
785
+ }
786
+ function applyFill(ctx, fill, width, height) {
787
+ switch (fill.type) {
788
+ case "solid":
789
+ ctx.fillStyle = colorToCSS(fill.color);
790
+ break;
791
+ case "linear-gradient": {
792
+ const rad = fill.angle * Math.PI / 180;
793
+ const cos = Math.cos(rad);
794
+ const sin = Math.sin(rad);
795
+ const halfW = width / 2;
796
+ const halfH = height / 2;
797
+ const grad = ctx.createLinearGradient(
798
+ halfW - cos * halfW,
799
+ halfH - sin * halfH,
800
+ halfW + cos * halfW,
801
+ halfH + sin * halfH
802
+ );
803
+ for (const stop of fill.stops) {
804
+ grad.addColorStop(stop.offset, colorToCSS(stop.color));
805
+ }
806
+ ctx.fillStyle = grad;
807
+ break;
808
+ }
809
+ case "radial-gradient": {
810
+ const cx = typeof fill.center.x === "number" ? fill.center.x : parseFloat(fill.center.x) / 100 * width;
811
+ const cy = typeof fill.center.y === "number" ? fill.center.y : parseFloat(fill.center.y) / 100 * height;
812
+ const r = typeof fill.radius === "number" ? fill.radius : parseFloat(fill.radius) / 100 * Math.max(width, height);
813
+ const grad = ctx.createRadialGradient(cx, cy, 0, cx, cy, r);
814
+ for (const stop of fill.stops) {
815
+ grad.addColorStop(stop.offset, colorToCSS(stop.color));
816
+ }
817
+ ctx.fillStyle = grad;
818
+ break;
819
+ }
820
+ }
821
+ }
822
+ function applyStroke(ctx, stroke, pathLength) {
823
+ ctx.strokeStyle = colorToCSS(stroke.color);
824
+ ctx.lineWidth = stroke.width;
825
+ if (stroke.lineCap) ctx.lineCap = stroke.lineCap;
826
+ if (stroke.lineJoin) ctx.lineJoin = stroke.lineJoin;
827
+ const start = stroke.strokeStart ?? 0;
828
+ const end = stroke.strokeEnd ?? 1;
829
+ if (start !== 0 || end !== 1) {
830
+ const visible = (end - start) * pathLength;
831
+ ctx.setLineDash([Math.max(visible, 0), pathLength + 1]);
832
+ ctx.lineDashOffset = -start * pathLength;
833
+ } else if (stroke.dash) {
834
+ ctx.setLineDash(stroke.dash);
835
+ }
836
+ }
837
+ function resolveUnit(value, reference) {
838
+ if (typeof value === "string" && value.endsWith("%")) {
839
+ return parseFloat(value) / 100 * reference;
840
+ }
841
+ return value;
842
+ }
843
+ function buildEffectiveLayer(resolved, parentWidth, parentHeight) {
844
+ const { layer, computedProperties } = resolved;
845
+ const cp = computedProperties;
846
+ const hasShadow = layer.shadow || cp["shadow.blur"] !== void 0 || cp["shadow.color"] !== void 0;
847
+ return {
848
+ layer,
849
+ visual: buildEffectiveVisual(layer.visual, cp),
850
+ x: resolveUnit(cp["frame.x"] ?? layer.frame.x, parentWidth),
851
+ y: resolveUnit(cp["frame.y"] ?? layer.frame.y, parentHeight),
852
+ width: resolveUnit(cp["bounds.width"] ?? layer.bounds.width, parentWidth),
853
+ height: resolveUnit(cp["bounds.height"] ?? layer.bounds.height, parentHeight),
854
+ opacity: cp["opacity"] ?? layer.opacity ?? 1,
855
+ rotation: cp["rotation"] ?? layer.rotation ?? 0,
856
+ scaleX: cp["scale.x"] ?? layer.scale?.x ?? 1,
857
+ scaleY: cp["scale.y"] ?? layer.scale?.y ?? 1,
858
+ anchorX: cp["anchorPoint.x"] ?? layer.anchorPoint?.x ?? 0,
859
+ anchorY: cp["anchorPoint.y"] ?? layer.anchorPoint?.y ?? 0,
860
+ shadow: hasShadow ? {
861
+ color: colorToCSS(cp["shadow.color"] ?? layer.shadow?.color ?? "#00000080"),
862
+ blur: cp["shadow.blur"] ?? layer.shadow?.blur ?? 0,
863
+ offsetX: cp["shadow.offsetX"] ?? layer.shadow?.offsetX ?? 0,
864
+ offsetY: cp["shadow.offsetY"] ?? layer.shadow?.offsetY ?? 0
865
+ } : void 0
866
+ };
867
+ }
868
+ function buildEffectiveVisual(visual, cp) {
869
+ const hasVisualOverride = cp["visual.fill.color"] !== void 0 || cp["visual.stroke.color"] !== void 0 || cp["visual.stroke.width"] !== void 0 || cp["visual.stroke.start"] !== void 0 || cp["visual.stroke.end"] !== void 0 || cp["visual.shape.cornerRadius"] !== void 0 || cp["visual.style.fontSize"] !== void 0 || cp["visual.style.color"] !== void 0;
870
+ if (!hasVisualOverride) return visual;
871
+ if (visual.type === "shape") {
872
+ const v = { ...visual };
873
+ if (cp["visual.shape.cornerRadius"] !== void 0 && v.shape.type === "rect") {
874
+ v.shape = { ...v.shape, cornerRadius: cp["visual.shape.cornerRadius"] };
875
+ }
876
+ if (v.fill && cp["visual.fill.color"] !== void 0) {
877
+ v.fill = v.fill.type === "solid" ? { ...v.fill, color: cp["visual.fill.color"] } : v.fill;
878
+ }
879
+ if (v.stroke) {
880
+ const strokeColor = cp["visual.stroke.color"] ?? v.stroke.color;
881
+ const strokeWidth = cp["visual.stroke.width"] ?? v.stroke.width;
882
+ const strokeStart = cp["visual.stroke.start"] ?? v.stroke.strokeStart;
883
+ const strokeEnd = cp["visual.stroke.end"] ?? v.stroke.strokeEnd;
884
+ if (strokeColor !== v.stroke.color || strokeWidth !== v.stroke.width || strokeStart !== v.stroke.strokeStart || strokeEnd !== v.stroke.strokeEnd) {
885
+ v.stroke = {
886
+ ...v.stroke,
887
+ color: strokeColor,
888
+ width: strokeWidth,
889
+ strokeStart,
890
+ strokeEnd
891
+ };
892
+ }
893
+ }
894
+ return v;
895
+ }
896
+ if (visual.type === "text") {
897
+ const v = { ...visual };
898
+ const fontSize = cp["visual.style.fontSize"] ?? v.style.fontSize;
899
+ const color = cp["visual.style.color"] ?? v.style.color;
900
+ if (fontSize !== v.style.fontSize || color !== v.style.color) {
901
+ v.style = { ...v.style, fontSize, color };
902
+ }
903
+ return v;
904
+ }
905
+ return visual;
906
+ }
907
+ function renderShape(ctx, eff) {
908
+ const visual = eff.visual;
909
+ const { shape } = visual;
910
+ const { width, height } = eff;
911
+ switch (shape.type) {
912
+ case "rect":
913
+ renderRect(ctx, width, height, shape.cornerRadius, visual);
914
+ break;
915
+ case "ellipse":
916
+ renderEllipse(ctx, width, height, visual);
917
+ break;
918
+ case "path":
919
+ renderPath(ctx, shape.points, shape.closed, visual);
920
+ break;
921
+ }
922
+ }
923
+ function rectPerimeter(w, h) {
924
+ return 2 * (w + h);
925
+ }
926
+ function ellipsePerimeter(w, h) {
927
+ const a = w / 2;
928
+ const b = h / 2;
929
+ return Math.PI * (3 * (a + b) - Math.sqrt((3 * a + b) * (a + 3 * b)));
930
+ }
931
+ function dist(x1, y1, x2, y2) {
932
+ const dx = x2 - x1;
933
+ const dy = y2 - y1;
934
+ return Math.sqrt(dx * dx + dy * dy);
935
+ }
936
+ function pathPerimeter(points, closed) {
937
+ let length = 0;
938
+ for (let i = 1; i < points.length; i++) {
939
+ const prev = points[i - 1];
940
+ const curr = points[i];
941
+ if (prev.out && curr.in) {
942
+ length += dist(prev.x, prev.y, curr.x, curr.y) * 1.2;
943
+ } else {
944
+ length += dist(prev.x, prev.y, curr.x, curr.y);
945
+ }
946
+ }
947
+ if (closed && points.length > 1) {
948
+ const first = points[0];
949
+ const last = points[points.length - 1];
950
+ length += dist(last.x, last.y, first.x, first.y);
951
+ }
952
+ return length;
953
+ }
954
+ function renderRect(ctx, width, height, cornerRadius, visual) {
955
+ if (visual.fill) {
956
+ applyFill(ctx, visual.fill, width, height);
957
+ if (cornerRadius && ctx.roundRect) {
958
+ ctx.beginPath();
959
+ ctx.roundRect(0, 0, width, height, cornerRadius);
960
+ ctx.fill();
961
+ } else {
962
+ ctx.fillRect(0, 0, width, height);
963
+ }
964
+ }
965
+ if (visual.stroke) {
966
+ const perimeter = rectPerimeter(width, height);
967
+ applyStroke(ctx, visual.stroke, perimeter);
968
+ if (cornerRadius && ctx.roundRect) {
969
+ ctx.beginPath();
970
+ ctx.roundRect(0, 0, width, height, cornerRadius);
971
+ ctx.stroke();
972
+ } else {
973
+ ctx.strokeRect(0, 0, width, height);
974
+ }
975
+ }
976
+ }
977
+ function renderEllipse(ctx, width, height, visual) {
978
+ ctx.beginPath();
979
+ ctx.ellipse(width / 2, height / 2, width / 2, height / 2, 0, 0, Math.PI * 2);
980
+ if (visual.fill) {
981
+ applyFill(ctx, visual.fill, width, height);
982
+ ctx.fill();
983
+ }
984
+ if (visual.stroke) {
985
+ const perimeter = ellipsePerimeter(width, height);
986
+ applyStroke(ctx, visual.stroke, perimeter);
987
+ ctx.stroke();
988
+ }
989
+ }
990
+ function renderPath(ctx, points, closed, visual) {
991
+ if (points.length < 2) return;
992
+ ctx.beginPath();
993
+ ctx.moveTo(points[0].x, points[0].y);
994
+ for (let i = 1; i < points.length; i++) {
995
+ const prev = points[i - 1];
996
+ const curr = points[i];
997
+ if (prev.out && curr.in) {
998
+ ctx.bezierCurveTo(
999
+ prev.x + prev.out.x,
1000
+ prev.y + prev.out.y,
1001
+ curr.x + curr.in.x,
1002
+ curr.y + curr.in.y,
1003
+ curr.x,
1004
+ curr.y
1005
+ );
1006
+ } else {
1007
+ ctx.lineTo(curr.x, curr.y);
1008
+ }
1009
+ }
1010
+ if (closed) ctx.closePath();
1011
+ if (visual.fill) {
1012
+ applyFill(ctx, visual.fill, 0, 0);
1013
+ ctx.fill();
1014
+ }
1015
+ if (visual.stroke) {
1016
+ const perimeter = pathPerimeter(points, closed);
1017
+ applyStroke(ctx, visual.stroke, perimeter);
1018
+ ctx.stroke();
1019
+ }
1020
+ }
1021
+ function renderText(ctx, eff) {
1022
+ const visual = eff.visual;
1023
+ const { style } = visual;
1024
+ const fontStyle = style.fontStyle ?? "normal";
1025
+ const fontWeight = style.fontWeight ?? "normal";
1026
+ const fontSize = style.fontSize;
1027
+ const fontFamily = style.fontFamily;
1028
+ ctx.font = `${fontStyle} ${fontWeight} ${fontSize}px ${fontFamily}`;
1029
+ const align = style.textAlign ?? "left";
1030
+ ctx.textAlign = align;
1031
+ ctx.textBaseline = "top";
1032
+ ctx.fillStyle = colorToCSS(style.color);
1033
+ let textX = 0;
1034
+ if (align === "center") {
1035
+ textX = eff.width / 2;
1036
+ } else if (align === "right") {
1037
+ textX = eff.width;
1038
+ }
1039
+ ctx.fillText(visual.content, textX, 0);
1040
+ }
1041
+ function renderImage(ctx, eff, imageCache) {
1042
+ const visual = eff.visual;
1043
+ const src = visual.src;
1044
+ if (!src) return;
1045
+ const img = imageCache.get(src);
1046
+ if (!img) {
1047
+ imageCache.load(src);
1048
+ return;
1049
+ }
1050
+ ctx.drawImage(img, 0, 0, eff.width, eff.height);
1051
+ }
1052
+ function renderRef(ctx, eff) {
1053
+ const visual = eff.visual;
1054
+ const { width, height } = eff;
1055
+ ctx.strokeStyle = "#888888";
1056
+ ctx.lineWidth = 2;
1057
+ ctx.setLineDash([6, 4]);
1058
+ ctx.strokeRect(0, 0, width, height);
1059
+ ctx.setLineDash([]);
1060
+ const label = `REF: ${visual.src}`;
1061
+ ctx.fillStyle = "#888888";
1062
+ ctx.font = `${Math.max(12, Math.min(16, height * 0.15))}px sans-serif`;
1063
+ ctx.textAlign = "center";
1064
+ ctx.textBaseline = "middle";
1065
+ ctx.fillText(label, width / 2, height / 2, width - 8);
1066
+ }
1067
+ function renderFrame(ctx, resolvedFrame, doc, imageCache) {
1068
+ const { width, height } = doc.canvas;
1069
+ ctx.fillStyle = doc.canvas.background ?? "transparent";
1070
+ ctx.fillRect(0, 0, width, height);
1071
+ const effMap = /* @__PURE__ */ new Map();
1072
+ const effList = [];
1073
+ for (const resolvedLayer of resolvedFrame.layers) {
1074
+ const eff = buildEffectiveLayer(resolvedLayer, width, height);
1075
+ effMap.set(resolvedLayer.layer.id, eff);
1076
+ effList.push(eff);
1077
+ }
1078
+ for (const eff of effList) {
1079
+ const { layer } = eff;
1080
+ if (layer.visible === false) continue;
1081
+ if (eff.opacity <= 0) continue;
1082
+ if (layer.visual.type === "image") {
1083
+ const iv = eff.visual;
1084
+ if (!iv.src && iv.assetId && doc.assets?.[iv.assetId]) {
1085
+ iv.src = doc.assets[iv.assetId].src;
1086
+ }
1087
+ }
1088
+ ctx.save();
1089
+ applyAncestorTransforms(ctx, layer.id, effMap, doc);
1090
+ ctx.globalAlpha = eff.opacity;
1091
+ ctx.translate(eff.x, eff.y);
1092
+ const anchorPixelX = eff.anchorX * eff.width;
1093
+ const anchorPixelY = eff.anchorY * eff.height;
1094
+ if (eff.rotation !== 0 || eff.scaleX !== 1 || eff.scaleY !== 1) {
1095
+ ctx.translate(anchorPixelX, anchorPixelY);
1096
+ if (eff.rotation !== 0) {
1097
+ ctx.rotate(eff.rotation * Math.PI / 180);
1098
+ }
1099
+ if (eff.scaleX !== 1 || eff.scaleY !== 1) {
1100
+ ctx.scale(eff.scaleX, eff.scaleY);
1101
+ }
1102
+ ctx.translate(-anchorPixelX, -anchorPixelY);
1103
+ }
1104
+ if (eff.shadow) {
1105
+ ctx.shadowColor = eff.shadow.color;
1106
+ ctx.shadowBlur = eff.shadow.blur;
1107
+ ctx.shadowOffsetX = eff.shadow.offsetX;
1108
+ ctx.shadowOffsetY = eff.shadow.offsetY;
1109
+ }
1110
+ switch (layer.visual.type) {
1111
+ case "shape":
1112
+ renderShape(ctx, eff);
1113
+ break;
1114
+ case "text":
1115
+ renderText(ctx, eff);
1116
+ break;
1117
+ case "image":
1118
+ if (imageCache) renderImage(ctx, eff, imageCache);
1119
+ break;
1120
+ case "group":
1121
+ break;
1122
+ case "ref":
1123
+ renderRef(ctx, eff);
1124
+ break;
1125
+ }
1126
+ ctx.restore();
1127
+ }
1128
+ }
1129
+ function applyAncestorTransforms(ctx, layerId, effMap, doc) {
1130
+ const chain = [];
1131
+ const layer = doc.layers.find((l) => l.id === layerId);
1132
+ let parentId = layer?.parentId;
1133
+ const visited = /* @__PURE__ */ new Set();
1134
+ while (parentId && !visited.has(parentId)) {
1135
+ visited.add(parentId);
1136
+ const parentEff = effMap.get(parentId);
1137
+ if (!parentEff) break;
1138
+ chain.push(parentEff);
1139
+ parentId = parentEff.layer.parentId;
1140
+ }
1141
+ for (let i = chain.length - 1; i >= 0; i--) {
1142
+ const p = chain[i];
1143
+ ctx.translate(p.x, p.y);
1144
+ const ax = p.anchorX * p.width;
1145
+ const ay = p.anchorY * p.height;
1146
+ if (p.rotation !== 0 || p.scaleX !== 1 || p.scaleY !== 1) {
1147
+ ctx.translate(ax, ay);
1148
+ if (p.rotation !== 0) ctx.rotate(p.rotation * Math.PI / 180);
1149
+ if (p.scaleX !== 1 || p.scaleY !== 1) ctx.scale(p.scaleX, p.scaleY);
1150
+ ctx.translate(-ax, -ay);
1151
+ }
1152
+ }
1153
+ }
1154
+ var ImageCache = class {
1155
+ cache = /* @__PURE__ */ new Map();
1156
+ loading = /* @__PURE__ */ new Set();
1157
+ onLoad;
1158
+ createImage;
1159
+ constructor(opts) {
1160
+ this.onLoad = opts?.onLoad;
1161
+ this.createImage = opts?.createImage;
1162
+ }
1163
+ get(src) {
1164
+ const entry = this.cache.get(src);
1165
+ return entry?.complete ? entry.image : null;
1166
+ }
1167
+ load(src) {
1168
+ if (this.cache.has(src) || this.loading.has(src)) return;
1169
+ if (!this.createImage) return;
1170
+ this.loading.add(src);
1171
+ const image = this.createImage(
1172
+ src,
1173
+ () => {
1174
+ this.cache.set(src, { complete: true, image });
1175
+ this.loading.delete(src);
1176
+ this.onLoad?.();
1177
+ },
1178
+ () => {
1179
+ this.loading.delete(src);
1180
+ }
1181
+ );
1182
+ }
1183
+ };
1184
+
1185
+ // src/commands/render-pipeline.ts
1186
+ async function checkFfmpeg() {
1187
+ return new Promise((resolve5) => {
1188
+ const proc = spawn("ffmpeg", ["-version"], { stdio: "pipe" });
1189
+ proc.on("error", () => resolve5(false));
1190
+ proc.on("close", (code) => resolve5(code === 0));
1191
+ });
1192
+ }
1193
+ function buildFfmpegArgs(width, height, fps, format, output) {
1194
+ const input = [
1195
+ "-y",
1196
+ "-f",
1197
+ "rawvideo",
1198
+ "-pix_fmt",
1199
+ "bgra",
1200
+ "-s",
1201
+ `${width}x${height}`,
1202
+ "-r",
1203
+ String(fps),
1204
+ "-i",
1205
+ "pipe:0"
1206
+ ];
1207
+ if (format === "mp4") {
1208
+ return [
1209
+ ...input,
1210
+ "-c:v",
1211
+ "libx264",
1212
+ "-pix_fmt",
1213
+ "yuv420p",
1214
+ "-preset",
1215
+ "medium",
1216
+ "-crf",
1217
+ "18",
1218
+ "-movflags",
1219
+ "+faststart",
1220
+ output
1221
+ ];
1222
+ }
1223
+ return [
1224
+ ...input,
1225
+ "-vf",
1226
+ "split[s0][s1];[s0]palettegen=stats_mode=single[p];[s1][p]paletteuse=dither=sierra2_4a",
1227
+ "-loop",
1228
+ "0",
1229
+ output
1230
+ ];
1231
+ }
1232
+ async function preloadImages(doc, loadImage) {
1233
+ const sources = /* @__PURE__ */ new Set();
1234
+ for (const layer of doc.layers) {
1235
+ if (layer.visual.type === "image") {
1236
+ const iv = layer.visual;
1237
+ if (iv.src) {
1238
+ sources.add(iv.src);
1239
+ } else if (iv.assetId && doc.assets?.[iv.assetId]) {
1240
+ sources.add(doc.assets[iv.assetId].src);
1241
+ }
1242
+ }
1243
+ }
1244
+ if (sources.size === 0) {
1245
+ return new ImageCache();
1246
+ }
1247
+ const preloaded = /* @__PURE__ */ new Map();
1248
+ await Promise.all(
1249
+ [...sources].map(async (src) => {
1250
+ try {
1251
+ preloaded.set(src, await loadImage(src));
1252
+ } catch {
1253
+ }
1254
+ })
1255
+ );
1256
+ const imageCache = new ImageCache({
1257
+ createImage: (src, onLoad, onError) => {
1258
+ const img = preloaded.get(src);
1259
+ if (img) {
1260
+ process.nextTick(onLoad);
1261
+ return img;
1262
+ }
1263
+ process.nextTick(onError);
1264
+ return {};
1265
+ }
1266
+ });
1267
+ for (const src of preloaded.keys()) {
1268
+ imageCache.load(src);
1269
+ }
1270
+ await new Promise((resolve5) => process.nextTick(resolve5));
1271
+ return imageCache;
1272
+ }
1273
+ async function renderDocument(doc, opts) {
1274
+ const canvasModuleName = "canvas";
1275
+ let createCanvas;
1276
+ let loadImage;
1277
+ try {
1278
+ const canvasModule = await import(
1279
+ /* webpackIgnore: true */
1280
+ canvasModuleName
1281
+ );
1282
+ createCanvas = canvasModule.createCanvas;
1283
+ loadImage = canvasModule.loadImage;
1284
+ } catch {
1285
+ throw new Error(
1286
+ "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"
1287
+ );
1288
+ }
1289
+ const { width, height, fps } = doc.canvas;
1290
+ const { output, format, states, onProgress } = opts;
1291
+ if (format === "mp4" && (width % 2 !== 0 || height % 2 !== 0)) {
1292
+ throw new Error(
1293
+ `H.264 requires even dimensions. Canvas is ${width}\xD7${height}. Try ${width + width % 2}\xD7${height + height % 2}.`
1294
+ );
1295
+ }
1296
+ const allStates = Object.keys(doc.states);
1297
+ const renderStates = states ?? allStates;
1298
+ for (const s of renderStates) {
1299
+ if (!(s in doc.states)) {
1300
+ throw new Error(
1301
+ `State "${s}" not found. Available: ${allStates.join(", ")}`
1302
+ );
1303
+ }
1304
+ }
1305
+ let totalFrames = 0;
1306
+ for (const s of renderStates) {
1307
+ totalFrames += doc.states[s].duration;
1308
+ }
1309
+ if (totalFrames === 0) {
1310
+ throw new Error("Nothing to render \u2014 all states have duration 0");
1311
+ }
1312
+ const imageCache = await preloadImages(doc, loadImage);
1313
+ const canvas = createCanvas(width, height);
1314
+ const ctx = canvas.getContext("2d");
1315
+ const ffmpegArgs = buildFfmpegArgs(width, height, fps, format, output);
1316
+ const ffmpeg = spawn("ffmpeg", ffmpegArgs, {
1317
+ stdio: ["pipe", "pipe", "pipe"]
1318
+ });
1319
+ let stderrOutput = "";
1320
+ ffmpeg.stderr?.on("data", (chunk) => {
1321
+ stderrOutput += chunk.toString();
1322
+ });
1323
+ const startTime = Date.now();
1324
+ let frameIndex = 0;
1325
+ for (const stateName of renderStates) {
1326
+ const duration = doc.states[stateName].duration;
1327
+ for (let f = 0; f < duration; f++) {
1328
+ const resolved = resolveFrame(doc, stateName, f);
1329
+ renderFrame(ctx, resolved, doc, imageCache);
1330
+ const raw = canvas.toBuffer("raw");
1331
+ const canWrite = ffmpeg.stdin.write(raw);
1332
+ if (!canWrite) {
1333
+ await new Promise(
1334
+ (resolve5) => ffmpeg.stdin.once("drain", resolve5)
1335
+ );
1336
+ }
1337
+ frameIndex++;
1338
+ onProgress?.({
1339
+ frame: frameIndex,
1340
+ totalFrames,
1341
+ state: stateName,
1342
+ percent: Math.round(frameIndex / totalFrames * 100)
1343
+ });
1344
+ }
1345
+ }
1346
+ ffmpeg.stdin.end();
1347
+ const exitCode = await new Promise((resolve5) => {
1348
+ ffmpeg.on("close", resolve5);
1349
+ });
1350
+ if (exitCode !== 0) {
1351
+ throw new Error(
1352
+ `FFmpeg exited with code ${exitCode}.
1353
+ ${stderrOutput.slice(-500)}`
1354
+ );
1355
+ }
1356
+ return {
1357
+ output,
1358
+ format,
1359
+ totalFrames,
1360
+ states: renderStates,
1361
+ durationMs: Date.now() - startTime
1362
+ };
1363
+ }
1364
+
1365
+ // src/commands/render.ts
1366
+ import { readFileSync as readFileSync4 } from "fs";
1367
+ import { resolve as resolve4, basename, extname } from "path";
1368
+ function readAndParse3(file) {
1369
+ const absPath = resolve4(file);
1370
+ let content;
1371
+ try {
1372
+ content = readFileSync4(absPath, "utf-8");
1373
+ } catch {
1374
+ console.error(`Cannot read file: ${absPath}`);
1375
+ return process.exit(1);
1376
+ }
1377
+ const result = parseAtelier(content);
1378
+ if (!result.success) {
1379
+ console.error("Parse errors:");
1380
+ for (const error of result.errors) {
1381
+ console.error(` - ${error.path}: ${error.message}`);
1382
+ }
1383
+ return process.exit(1);
1384
+ }
1385
+ return result.data;
1386
+ }
1387
+ function inferFormat(output) {
1388
+ if (!output) return "mp4";
1389
+ const ext = extname(output).toLowerCase();
1390
+ if (ext === ".gif") return "gif";
1391
+ return "mp4";
1392
+ }
1393
+ function renderCommand(program) {
1394
+ 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(
1395
+ "-s, --state <names...>",
1396
+ "State(s) to render (default: all in order)"
1397
+ ).action(
1398
+ async (file, options) => {
1399
+ const hasFfmpeg = await checkFfmpeg();
1400
+ if (!hasFfmpeg) {
1401
+ console.error("FFmpeg is not installed or not in PATH.");
1402
+ console.error("Install it:");
1403
+ console.error(" macOS: brew install ffmpeg");
1404
+ console.error(" Ubuntu: sudo apt install ffmpeg");
1405
+ console.error(" Windows: https://ffmpeg.org/download.html");
1406
+ process.exit(1);
1407
+ return;
1408
+ }
1409
+ const doc = readAndParse3(file);
1410
+ let format;
1411
+ if (options.format) {
1412
+ if (options.format !== "mp4" && options.format !== "gif") {
1413
+ console.error(
1414
+ `Unknown format: "${options.format}". Use mp4 or gif.`
1415
+ );
1416
+ process.exit(1);
1417
+ return;
1418
+ }
1419
+ format = options.format;
1420
+ } else {
1421
+ format = inferFormat(options.output);
1422
+ }
1423
+ const inputName = basename(file, extname(file));
1424
+ const output = options.output ?? `${inputName}.${format}`;
1425
+ const startTime = Date.now();
1426
+ try {
1427
+ const result = await renderDocument(doc, {
1428
+ output: resolve4(output),
1429
+ format,
1430
+ states: options.state,
1431
+ onProgress: ({ frame, totalFrames, state, percent }) => {
1432
+ const elapsed = (Date.now() - startTime) / 1e3;
1433
+ const rate = elapsed > 0 ? frame / elapsed : 0;
1434
+ const remaining = rate > 0 ? (totalFrames - frame) / rate : 0;
1435
+ process.stderr.write(
1436
+ `\rRendering: frame ${frame}/${totalFrames} (${percent}%) - state "${state}" - ETA ${remaining.toFixed(1)}s`
1437
+ );
1438
+ }
1439
+ });
1440
+ process.stderr.write("\n");
1441
+ console.log(
1442
+ `Done: ${result.totalFrames} frames \u2192 ${result.output} (${(result.durationMs / 1e3).toFixed(1)}s)`
1443
+ );
1444
+ } catch (err) {
1445
+ process.stderr.write("\n");
1446
+ console.error(err.message);
1447
+ process.exit(1);
1448
+ }
1449
+ }
1450
+ );
1451
+ }
1452
+
1453
+ export {
1454
+ validateFile,
1455
+ validateCommand,
1456
+ getInfo,
1457
+ infoCommand,
1458
+ resolveStill,
1459
+ stillCommand,
1460
+ checkFfmpeg,
1461
+ buildFfmpegArgs,
1462
+ renderDocument,
1463
+ renderCommand
1464
+ };
1465
+ //# sourceMappingURL=chunk-LL2EJ6YE.js.map