@a-company/atelier-studio 0.25.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,3621 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ AtelierStudio: () => AtelierStudio,
34
+ applyStudioTheme: () => applyStudioTheme,
35
+ defaultTheme: () => defaultTheme,
36
+ exportDocument: () => exportDocument,
37
+ supportsGifExport: () => supportsGifExport,
38
+ supportsWebCodecs: () => supportsWebCodecs,
39
+ supportsWebM: () => supportsWebM
40
+ });
41
+ module.exports = __toCommonJS(index_exports);
42
+
43
+ // src/panels/canvas-panel.ts
44
+ var import_atelier_core = require("@a-company/atelier-core");
45
+ var import_atelier_canvas = require("@a-company/atelier-canvas");
46
+
47
+ // src/player.ts
48
+ var Player = class {
49
+ constructor(fps, totalFrames, onFrame, loop = true) {
50
+ this.fps = fps;
51
+ this.totalFrames = totalFrames;
52
+ this.onFrame = onFrame;
53
+ this.loop = loop;
54
+ }
55
+ animationId = null;
56
+ currentFrame = 0;
57
+ playing = false;
58
+ lastTimestamp = 0;
59
+ accumulator = 0;
60
+ play() {
61
+ if (this.playing) return;
62
+ this.playing = true;
63
+ this.lastTimestamp = 0;
64
+ this.accumulator = 0;
65
+ this.animationId = requestAnimationFrame((ts) => this.tick(ts));
66
+ }
67
+ pause() {
68
+ this.playing = false;
69
+ if (this.animationId !== null) {
70
+ cancelAnimationFrame(this.animationId);
71
+ this.animationId = null;
72
+ }
73
+ }
74
+ seek(frame) {
75
+ this.currentFrame = Math.max(0, Math.min(frame, this.totalFrames - 1));
76
+ this.onFrame(this.currentFrame);
77
+ }
78
+ toggle() {
79
+ if (this.playing) this.pause();
80
+ else this.play();
81
+ }
82
+ get isPlaying() {
83
+ return this.playing;
84
+ }
85
+ get frame() {
86
+ return this.currentFrame;
87
+ }
88
+ setTotalFrames(total) {
89
+ this.totalFrames = total;
90
+ if (this.currentFrame >= total) {
91
+ this.currentFrame = 0;
92
+ }
93
+ }
94
+ destroy() {
95
+ this.pause();
96
+ }
97
+ tick(timestamp) {
98
+ if (!this.playing) return;
99
+ if (this.lastTimestamp === 0) this.lastTimestamp = timestamp;
100
+ const elapsed = timestamp - this.lastTimestamp;
101
+ this.lastTimestamp = timestamp;
102
+ this.accumulator += elapsed;
103
+ const frameDuration = 1e3 / this.fps;
104
+ let frameChanged = false;
105
+ while (this.accumulator >= frameDuration) {
106
+ this.accumulator -= frameDuration;
107
+ this.currentFrame++;
108
+ if (this.currentFrame >= this.totalFrames) {
109
+ if (this.loop) {
110
+ this.currentFrame = 0;
111
+ } else {
112
+ this.currentFrame = this.totalFrames - 1;
113
+ this.pause();
114
+ this.onFrame(this.currentFrame);
115
+ return;
116
+ }
117
+ }
118
+ frameChanged = true;
119
+ }
120
+ if (frameChanged) this.onFrame(this.currentFrame);
121
+ this.animationId = requestAnimationFrame((ts) => this.tick(ts));
122
+ }
123
+ };
124
+
125
+ // src/panels/canvas-panel.ts
126
+ var CanvasPanel = class {
127
+ constructor(parent, opts = {}) {
128
+ this.opts = opts;
129
+ this.el = document.createElement("div");
130
+ this.el.className = "atel-canvas-panel";
131
+ const wrap = document.createElement("div");
132
+ wrap.className = "atel-canvas-wrap";
133
+ this.canvas = document.createElement("canvas");
134
+ this.canvas.width = 600;
135
+ this.canvas.height = 400;
136
+ this.ctx = this.canvas.getContext("2d");
137
+ this.imageCache = new import_atelier_canvas.ImageCache({
138
+ onLoad: () => this.refresh(),
139
+ createImage: (src, onLoad, onError) => {
140
+ const img = new Image();
141
+ img.onload = onLoad;
142
+ img.onerror = onError;
143
+ img.src = src;
144
+ return img;
145
+ }
146
+ });
147
+ wrap.appendChild(this.canvas);
148
+ this.el.appendChild(wrap);
149
+ const playback = document.createElement("div");
150
+ playback.className = "atel-playback";
151
+ this.playBtn = document.createElement("button");
152
+ this.playBtn.className = "atel-playback__btn";
153
+ this.playBtn.textContent = "\u25B6";
154
+ this.scrubber = document.createElement("input");
155
+ this.scrubber.type = "range";
156
+ this.scrubber.className = "atel-playback__scrubber";
157
+ this.scrubber.min = "0";
158
+ this.scrubber.max = "0";
159
+ this.scrubber.value = "0";
160
+ this.frameDisplay = document.createElement("span");
161
+ this.frameDisplay.className = "atel-playback__frame";
162
+ this.frameDisplay.textContent = "0 / 0";
163
+ playback.appendChild(this.playBtn);
164
+ playback.appendChild(this.scrubber);
165
+ playback.appendChild(this.frameDisplay);
166
+ this.el.appendChild(playback);
167
+ parent.appendChild(this.el);
168
+ this.playBtn.addEventListener("click", () => this.togglePlay());
169
+ let wasPlaying = false;
170
+ this.scrubber.addEventListener("mousedown", () => {
171
+ wasPlaying = this.player?.isPlaying ?? false;
172
+ this.player?.pause();
173
+ this.updatePlayBtn();
174
+ });
175
+ this.scrubber.addEventListener("input", () => {
176
+ this.player?.seek(Number(this.scrubber.value));
177
+ });
178
+ this.scrubber.addEventListener("mouseup", () => {
179
+ if (wasPlaying) {
180
+ this.player?.play();
181
+ this.updatePlayBtn();
182
+ }
183
+ });
184
+ }
185
+ el;
186
+ canvas;
187
+ ctx;
188
+ player = null;
189
+ playBtn;
190
+ scrubber;
191
+ frameDisplay;
192
+ doc = null;
193
+ stateName = "";
194
+ imageCache;
195
+ loadDocument(doc, stateName) {
196
+ this.doc = doc;
197
+ this.stateName = stateName ?? Object.keys(doc.states)[0];
198
+ const state = doc.states[this.stateName];
199
+ if (!state) return;
200
+ this.canvas.width = doc.canvas.width;
201
+ this.canvas.height = doc.canvas.height;
202
+ const maxWidth = 500;
203
+ if (doc.canvas.width > maxWidth) {
204
+ const scale = maxWidth / doc.canvas.width;
205
+ this.canvas.style.width = `${doc.canvas.width * scale}px`;
206
+ this.canvas.style.height = `${doc.canvas.height * scale}px`;
207
+ } else {
208
+ this.canvas.style.width = "";
209
+ this.canvas.style.height = "";
210
+ }
211
+ const totalFrames = state.duration;
212
+ this.scrubber.max = String(totalFrames - 1);
213
+ this.scrubber.value = "0";
214
+ if (this.player) this.player.destroy();
215
+ this.player = new Player(doc.canvas.fps, totalFrames, (f) => this.renderAt(f), true);
216
+ this.renderAt(0);
217
+ this.player.play();
218
+ this.updatePlayBtn();
219
+ }
220
+ setStateName(name) {
221
+ if (!this.doc) return;
222
+ this.stateName = name;
223
+ const state = this.doc.states[name];
224
+ if (!state) return;
225
+ this.scrubber.max = String(state.duration - 1);
226
+ this.scrubber.value = "0";
227
+ this.player?.setTotalFrames(state.duration);
228
+ this.player?.seek(0);
229
+ }
230
+ renderAt(frame) {
231
+ if (!this.doc) return;
232
+ const resolved = (0, import_atelier_core.resolveFrame)(this.doc, this.stateName, frame);
233
+ (0, import_atelier_canvas.renderFrame)(this.ctx, resolved, this.doc, this.imageCache);
234
+ this.scrubber.value = String(frame);
235
+ const total = this.doc.states[this.stateName]?.duration ?? 0;
236
+ this.frameDisplay.textContent = `${frame} / ${total - 1}`;
237
+ this.opts.onFrameChange?.(frame);
238
+ }
239
+ /** Re-render current frame (after doc mutation) */
240
+ refresh() {
241
+ if (!this.doc || !this.player) return;
242
+ this.renderAt(this.player.frame);
243
+ }
244
+ togglePlay() {
245
+ this.player?.toggle();
246
+ this.updatePlayBtn();
247
+ }
248
+ stepForward() {
249
+ if (this.player) this.player.seek(this.player.frame + 1);
250
+ }
251
+ stepBack() {
252
+ if (this.player) this.player.seek(Math.max(0, this.player.frame - 1));
253
+ }
254
+ updatePlayBtn() {
255
+ this.playBtn.textContent = this.player?.isPlaying ? "\u275A\u275A" : "\u25B6";
256
+ }
257
+ getCanvas() {
258
+ return this.canvas;
259
+ }
260
+ getImageCache() {
261
+ return this.imageCache;
262
+ }
263
+ pause() {
264
+ this.player?.pause();
265
+ this.updatePlayBtn();
266
+ }
267
+ destroy() {
268
+ this.player?.destroy();
269
+ }
270
+ };
271
+
272
+ // src/utils/drag-reorder.ts
273
+ function enableDragReorder(opts) {
274
+ const { list, itemClass, onReorder } = opts;
275
+ const ac = new AbortController();
276
+ const signal = ac.signal;
277
+ let fromIndex = -1;
278
+ function getItems() {
279
+ return Array.from(list.querySelectorAll(`.${itemClass}`));
280
+ }
281
+ function setupHandles() {
282
+ for (const item of getItems()) {
283
+ const handle = item.querySelector(".atel-drag-handle");
284
+ if (!handle || handle.dataset.bound) continue;
285
+ handle.dataset.bound = "1";
286
+ handle.addEventListener(
287
+ "pointerdown",
288
+ () => {
289
+ item.draggable = true;
290
+ },
291
+ { signal }
292
+ );
293
+ handle.addEventListener(
294
+ "pointerup",
295
+ () => {
296
+ item.draggable = false;
297
+ },
298
+ { signal }
299
+ );
300
+ }
301
+ }
302
+ const observer = new MutationObserver(() => setupHandles());
303
+ observer.observe(list, { childList: true });
304
+ setupHandles();
305
+ list.addEventListener(
306
+ "dragstart",
307
+ (e) => {
308
+ const item = e.target.closest(`.${itemClass}`);
309
+ if (!item) return;
310
+ fromIndex = getItems().indexOf(item);
311
+ item.classList.add("atel-drag-item--dragging");
312
+ if (e.dataTransfer) e.dataTransfer.effectAllowed = "move";
313
+ },
314
+ { signal }
315
+ );
316
+ list.addEventListener(
317
+ "dragover",
318
+ (e) => {
319
+ e.preventDefault();
320
+ const items = getItems();
321
+ for (const it of items) {
322
+ it.classList.remove("atel-drag-indicator--above", "atel-drag-indicator--below");
323
+ }
324
+ const target = e.target.closest(`.${itemClass}`);
325
+ if (!target) return;
326
+ const rect = target.getBoundingClientRect();
327
+ const mid = rect.top + rect.height / 2;
328
+ if (e.clientY < mid) {
329
+ target.classList.add("atel-drag-indicator--above");
330
+ } else {
331
+ target.classList.add("atel-drag-indicator--below");
332
+ }
333
+ },
334
+ { signal }
335
+ );
336
+ list.addEventListener(
337
+ "drop",
338
+ (e) => {
339
+ e.preventDefault();
340
+ const items = getItems();
341
+ const target = e.target.closest(`.${itemClass}`);
342
+ if (!target || fromIndex < 0) return;
343
+ let toIndex = items.indexOf(target);
344
+ const rect = target.getBoundingClientRect();
345
+ const mid = rect.top + rect.height / 2;
346
+ if (e.clientY >= mid) toIndex++;
347
+ if (fromIndex < toIndex) toIndex--;
348
+ if (fromIndex !== toIndex && toIndex >= 0) {
349
+ onReorder(fromIndex, toIndex);
350
+ }
351
+ cleanup();
352
+ },
353
+ { signal }
354
+ );
355
+ list.addEventListener(
356
+ "dragend",
357
+ () => {
358
+ cleanup();
359
+ },
360
+ { signal }
361
+ );
362
+ function cleanup() {
363
+ fromIndex = -1;
364
+ for (const item of getItems()) {
365
+ item.draggable = false;
366
+ item.classList.remove(
367
+ "atel-drag-item--dragging",
368
+ "atel-drag-indicator--above",
369
+ "atel-drag-indicator--below"
370
+ );
371
+ }
372
+ }
373
+ return {
374
+ destroy() {
375
+ ac.abort();
376
+ observer.disconnect();
377
+ }
378
+ };
379
+ }
380
+
381
+ // src/panels/layer-panel.ts
382
+ var TYPE_ICONS = {
383
+ shape: "\u25A0",
384
+ text: "T",
385
+ image: "\u25A3",
386
+ group: "\u25A8",
387
+ ref: "\u2192"
388
+ };
389
+ var LayerPanel = class {
390
+ constructor(parent, opts) {
391
+ this.opts = opts;
392
+ this.el = document.createElement("div");
393
+ this.el.className = "atel-layer-panel";
394
+ const header = document.createElement("div");
395
+ header.className = "atel-panel-header";
396
+ header.textContent = "Layers";
397
+ this.el.appendChild(header);
398
+ this.list = document.createElement("div");
399
+ this.list.className = "atel-layer-list";
400
+ this.el.appendChild(this.list);
401
+ enableDragReorder({
402
+ list: this.list,
403
+ itemClass: "atel-layer-item",
404
+ onReorder: (from, to) => this.reorderLayer(from, to)
405
+ });
406
+ const actions = document.createElement("div");
407
+ actions.className = "atel-layer-actions";
408
+ const addBtn = document.createElement("button");
409
+ addBtn.textContent = "+ Add";
410
+ addBtn.addEventListener("click", () => this.addLayer());
411
+ const removeBtn = document.createElement("button");
412
+ removeBtn.textContent = "- Remove";
413
+ removeBtn.addEventListener("click", () => this.removeLayer());
414
+ actions.appendChild(addBtn);
415
+ actions.appendChild(removeBtn);
416
+ this.el.appendChild(actions);
417
+ parent.appendChild(this.el);
418
+ }
419
+ el;
420
+ list;
421
+ doc = null;
422
+ selectedId = null;
423
+ loadDocument(doc) {
424
+ this.doc = doc;
425
+ this.selectedId = null;
426
+ this.render();
427
+ }
428
+ getSelectedId() {
429
+ return this.selectedId;
430
+ }
431
+ selectLayer(id) {
432
+ this.selectedId = id;
433
+ this.render();
434
+ this.opts.onSelectLayer(id);
435
+ }
436
+ removeSelectedLayer() {
437
+ this.removeLayer();
438
+ }
439
+ refresh() {
440
+ this.render();
441
+ }
442
+ render() {
443
+ const scrollTop = this.list.scrollTop;
444
+ this.list.innerHTML = "";
445
+ if (!this.doc) return;
446
+ for (const layer of this.doc.layers) {
447
+ const item = document.createElement("div");
448
+ item.className = `atel-layer-item${layer.id === this.selectedId ? " atel-layer-item--selected" : ""}`;
449
+ const handle = document.createElement("span");
450
+ handle.className = "atel-drag-handle";
451
+ handle.textContent = "\u2630";
452
+ const icon = document.createElement("span");
453
+ icon.className = "atel-layer-item__icon";
454
+ icon.textContent = TYPE_ICONS[layer.visual.type] ?? "?";
455
+ const idSpan = document.createElement("span");
456
+ idSpan.className = "atel-layer-item__id";
457
+ idSpan.textContent = layer.id;
458
+ const visBtn = document.createElement("button");
459
+ visBtn.className = "atel-layer-item__vis";
460
+ visBtn.textContent = layer.visible === false ? "\u25CB" : "\u25C9";
461
+ visBtn.addEventListener("click", (e) => {
462
+ e.stopPropagation();
463
+ layer.visible = layer.visible === false ? true : false;
464
+ this.render();
465
+ this.opts.onDocumentChange();
466
+ });
467
+ item.appendChild(handle);
468
+ item.appendChild(icon);
469
+ item.appendChild(idSpan);
470
+ item.appendChild(visBtn);
471
+ item.addEventListener("click", () => {
472
+ this.selectLayer(layer.id);
473
+ });
474
+ this.list.appendChild(item);
475
+ }
476
+ this.list.scrollTop = scrollTop;
477
+ }
478
+ reorderLayer(from, to) {
479
+ if (!this.doc) return;
480
+ const [moved] = this.doc.layers.splice(from, 1);
481
+ this.doc.layers.splice(to, 0, moved);
482
+ this.render();
483
+ this.opts.onDocumentChange();
484
+ }
485
+ addLayer() {
486
+ if (!this.doc) return;
487
+ const id = `layer-${this.doc.layers.length + 1}`;
488
+ const newLayer = {
489
+ id,
490
+ visual: {
491
+ type: "shape",
492
+ shape: { type: "rect" },
493
+ fill: { type: "solid", color: "#888888" }
494
+ },
495
+ frame: { x: 50, y: 50 },
496
+ bounds: { width: 100, height: 100 },
497
+ opacity: 1
498
+ };
499
+ this.doc.layers.push(newLayer);
500
+ this.selectLayer(id);
501
+ this.opts.onDocumentChange();
502
+ }
503
+ removeLayer() {
504
+ if (!this.doc || !this.selectedId) return;
505
+ const idx = this.doc.layers.findIndex((l) => l.id === this.selectedId);
506
+ if (idx === -1) return;
507
+ this.doc.layers.splice(idx, 1);
508
+ for (const state of Object.values(this.doc.states)) {
509
+ state.deltas = state.deltas.filter((d) => d.layer !== this.selectedId);
510
+ }
511
+ this.selectedId = null;
512
+ this.render();
513
+ this.opts.onSelectLayer(null);
514
+ this.opts.onDocumentChange();
515
+ }
516
+ };
517
+
518
+ // src/controls/number-input.ts
519
+ var NumberInput = class {
520
+ constructor(parent, opts) {
521
+ this.opts = opts;
522
+ this.el = document.createElement("div");
523
+ this.el.className = "atel-number-input";
524
+ const label = document.createElement("label");
525
+ label.className = "atel-control-label";
526
+ label.textContent = opts.label;
527
+ const wrap = document.createElement("div");
528
+ wrap.className = "atel-number-input__wrap";
529
+ this.input = document.createElement("input");
530
+ this.input.type = "number";
531
+ this.input.className = "atel-input";
532
+ this.input.value = String(opts.value);
533
+ if (opts.step !== void 0) this.input.step = String(opts.step);
534
+ if (opts.min !== void 0) this.input.min = String(opts.min);
535
+ if (opts.max !== void 0) this.input.max = String(opts.max);
536
+ wrap.appendChild(this.input);
537
+ if (opts.suffix) {
538
+ const suf = document.createElement("span");
539
+ suf.className = "atel-number-input__suffix";
540
+ suf.textContent = opts.suffix;
541
+ wrap.appendChild(suf);
542
+ }
543
+ this.el.appendChild(label);
544
+ this.el.appendChild(wrap);
545
+ parent.appendChild(this.el);
546
+ this.input.addEventListener("input", () => {
547
+ const v = parseFloat(this.input.value);
548
+ if (!isNaN(v)) opts.onChange(v);
549
+ });
550
+ }
551
+ el;
552
+ input;
553
+ setValue(v) {
554
+ this.input.value = String(v);
555
+ }
556
+ };
557
+
558
+ // src/controls/color-picker.ts
559
+ var ColorPicker = class {
560
+ constructor(parent, opts) {
561
+ this.opts = opts;
562
+ this.el = document.createElement("div");
563
+ this.el.className = "atel-color-picker";
564
+ const label = document.createElement("label");
565
+ label.className = "atel-control-label";
566
+ label.textContent = opts.label;
567
+ const wrap = document.createElement("div");
568
+ wrap.className = "atel-color-picker__wrap";
569
+ this.input = document.createElement("input");
570
+ this.input.type = "color";
571
+ this.input.className = "atel-color-picker__input";
572
+ this.input.value = normalizeHex(opts.value);
573
+ this.hex = document.createElement("span");
574
+ this.hex.className = "atel-color-picker__hex";
575
+ this.hex.textContent = this.input.value;
576
+ wrap.appendChild(this.input);
577
+ wrap.appendChild(this.hex);
578
+ this.el.appendChild(label);
579
+ this.el.appendChild(wrap);
580
+ parent.appendChild(this.el);
581
+ this.input.addEventListener("input", () => {
582
+ this.hex.textContent = this.input.value;
583
+ opts.onChange(this.input.value);
584
+ });
585
+ }
586
+ el;
587
+ input;
588
+ hex;
589
+ setValue(v) {
590
+ this.input.value = normalizeHex(v);
591
+ this.hex.textContent = this.input.value;
592
+ }
593
+ };
594
+ function normalizeHex(c) {
595
+ if (c.startsWith("#") && c.length >= 7) return c.slice(0, 7);
596
+ if (c.startsWith("#") && c.length === 4) {
597
+ return "#" + c[1] + c[1] + c[2] + c[2] + c[3] + c[3];
598
+ }
599
+ return c;
600
+ }
601
+
602
+ // src/controls/select-input.ts
603
+ var SelectInput = class {
604
+ constructor(parent, opts) {
605
+ this.opts = opts;
606
+ this.el = document.createElement("div");
607
+ this.el.className = "atel-select-input";
608
+ const label = document.createElement("label");
609
+ label.className = "atel-control-label";
610
+ label.textContent = opts.label;
611
+ this.select = document.createElement("select");
612
+ this.select.className = "atel-input";
613
+ for (const opt of opts.options) {
614
+ const o = document.createElement("option");
615
+ o.value = opt.value;
616
+ o.textContent = opt.label;
617
+ if (opt.value === opts.value) o.selected = true;
618
+ this.select.appendChild(o);
619
+ }
620
+ this.el.appendChild(label);
621
+ this.el.appendChild(this.select);
622
+ parent.appendChild(this.el);
623
+ this.select.addEventListener("change", () => {
624
+ opts.onChange(this.select.value);
625
+ });
626
+ }
627
+ el;
628
+ select;
629
+ setValue(v) {
630
+ this.select.value = v;
631
+ }
632
+ };
633
+
634
+ // src/controls/slider-input.ts
635
+ var SliderInput = class {
636
+ constructor(parent, opts) {
637
+ this.opts = opts;
638
+ this.el = document.createElement("div");
639
+ this.el.className = "atel-slider-input";
640
+ const label = document.createElement("label");
641
+ label.className = "atel-control-label";
642
+ label.textContent = opts.label;
643
+ const wrap = document.createElement("div");
644
+ wrap.className = "atel-slider-input__wrap";
645
+ this.input = document.createElement("input");
646
+ this.input.type = "range";
647
+ this.input.className = "atel-slider-input__range";
648
+ this.input.min = String(opts.min);
649
+ this.input.max = String(opts.max);
650
+ this.input.step = String(opts.step ?? 0.01);
651
+ this.input.value = String(opts.value);
652
+ this.display = document.createElement("span");
653
+ this.display.className = "atel-slider-input__value";
654
+ this.display.textContent = formatValue(opts.value);
655
+ wrap.appendChild(this.input);
656
+ wrap.appendChild(this.display);
657
+ this.el.appendChild(label);
658
+ this.el.appendChild(wrap);
659
+ parent.appendChild(this.el);
660
+ this.input.addEventListener("input", () => {
661
+ const v = parseFloat(this.input.value);
662
+ this.display.textContent = formatValue(v);
663
+ opts.onChange(v);
664
+ });
665
+ }
666
+ el;
667
+ input;
668
+ display;
669
+ setValue(v) {
670
+ this.input.value = String(v);
671
+ this.display.textContent = formatValue(v);
672
+ }
673
+ };
674
+ function formatValue(v) {
675
+ return v % 1 === 0 ? String(v) : v.toFixed(2);
676
+ }
677
+
678
+ // src/controls/gradient-editor.ts
679
+ var GradientEditor = class {
680
+ constructor(parent, opts) {
681
+ this.opts = opts;
682
+ this.el = document.createElement("div");
683
+ this.el.className = "atel-gradient-editor";
684
+ this.preview = document.createElement("div");
685
+ this.preview.className = "atel-gradient-editor__preview";
686
+ this.el.appendChild(this.preview);
687
+ this.stopsContainer = document.createElement("div");
688
+ this.stopsContainer.className = "atel-gradient-editor__stops";
689
+ this.el.appendChild(this.stopsContainer);
690
+ const addBtn = document.createElement("button");
691
+ addBtn.className = "atel-gradient-editor__add";
692
+ addBtn.textContent = "+ Add Stop";
693
+ addBtn.addEventListener("click", () => this.addStop());
694
+ this.el.appendChild(addBtn);
695
+ parent.appendChild(this.el);
696
+ this.buildStops();
697
+ this.updatePreview();
698
+ }
699
+ el;
700
+ stopsContainer;
701
+ preview;
702
+ buildStops() {
703
+ this.stopsContainer.innerHTML = "";
704
+ const sorted = this.sortedStops();
705
+ for (let i = 0; i < sorted.length; i++) {
706
+ const stop = sorted[i];
707
+ const row = document.createElement("div");
708
+ row.className = "atel-gradient-editor__stop";
709
+ const color = typeof stop.color === "string" ? stop.color : "#888888";
710
+ new ColorPicker(row, {
711
+ label: "",
712
+ value: color,
713
+ onChange: (v) => {
714
+ stop.color = v;
715
+ this.updatePreview();
716
+ this.opts.onChange(this.opts.stops);
717
+ }
718
+ });
719
+ new SliderInput(row, {
720
+ label: "",
721
+ value: stop.offset,
722
+ min: 0,
723
+ max: 1,
724
+ step: 0.01,
725
+ onChange: (v) => {
726
+ stop.offset = v;
727
+ this.updatePreview();
728
+ this.opts.onChange(this.opts.stops);
729
+ }
730
+ });
731
+ const removeBtn = document.createElement("button");
732
+ removeBtn.className = "atel-gradient-editor__remove";
733
+ removeBtn.textContent = "\xD7";
734
+ removeBtn.disabled = this.opts.stops.length <= 2;
735
+ removeBtn.addEventListener("click", () => {
736
+ const idx = this.opts.stops.indexOf(stop);
737
+ if (idx !== -1 && this.opts.stops.length > 2) {
738
+ this.opts.stops.splice(idx, 1);
739
+ this.buildStops();
740
+ this.updatePreview();
741
+ this.opts.onChange(this.opts.stops);
742
+ }
743
+ });
744
+ row.appendChild(removeBtn);
745
+ this.stopsContainer.appendChild(row);
746
+ }
747
+ }
748
+ addStop() {
749
+ const sorted = this.sortedStops();
750
+ const a = sorted[sorted.length - 2];
751
+ const b = sorted[sorted.length - 1];
752
+ const offset = (a.offset + b.offset) / 2;
753
+ const color = typeof b.color === "string" ? b.color : "#888888";
754
+ this.opts.stops.push({ offset, color });
755
+ this.buildStops();
756
+ this.updatePreview();
757
+ this.opts.onChange(this.opts.stops);
758
+ }
759
+ sortedStops() {
760
+ return [...this.opts.stops].sort((a, b) => a.offset - b.offset);
761
+ }
762
+ updatePreview() {
763
+ const sorted = this.sortedStops();
764
+ const parts = sorted.map((s) => {
765
+ const c = typeof s.color === "string" ? s.color : "#888888";
766
+ return `${c} ${(s.offset * 100).toFixed(0)}%`;
767
+ });
768
+ this.preview.style.background = `linear-gradient(90deg, ${parts.join(", ")})`;
769
+ }
770
+ };
771
+
772
+ // src/controls/fill-editor.ts
773
+ var FILL_TYPE_OPTIONS = [
774
+ { label: "Solid", value: "solid" },
775
+ { label: "Linear Gradient", value: "linear-gradient" },
776
+ { label: "Radial Gradient", value: "radial-gradient" }
777
+ ];
778
+ var FillEditor = class {
779
+ constructor(parent, opts) {
780
+ this.opts = opts;
781
+ this.el = document.createElement("div");
782
+ this.el.className = "atel-fill-editor";
783
+ new SelectInput(this.el, {
784
+ label: "Fill Type",
785
+ value: opts.fill.type,
786
+ options: FILL_TYPE_OPTIONS,
787
+ onChange: (v) => this.switchType(v)
788
+ });
789
+ this.body = document.createElement("div");
790
+ this.body.className = "atel-fill-editor__body";
791
+ this.el.appendChild(this.body);
792
+ parent.appendChild(this.el);
793
+ this.buildBody();
794
+ }
795
+ el;
796
+ body;
797
+ buildBody() {
798
+ this.body.innerHTML = "";
799
+ const fill = this.opts.fill;
800
+ if (fill.type === "solid") {
801
+ const color = typeof fill.color === "string" ? fill.color : "#888888";
802
+ new ColorPicker(this.body, {
803
+ label: "Color",
804
+ value: color,
805
+ onChange: (v) => {
806
+ if (this.opts.fill.type === "solid") {
807
+ this.opts.fill.color = v;
808
+ this.opts.onChange(this.opts.fill);
809
+ }
810
+ }
811
+ });
812
+ } else if (fill.type === "linear-gradient") {
813
+ new NumberInput(this.body, {
814
+ label: "Angle",
815
+ value: fill.angle,
816
+ step: 1,
817
+ suffix: "\xB0",
818
+ onChange: (v) => {
819
+ if (this.opts.fill.type === "linear-gradient") {
820
+ this.opts.fill.angle = v;
821
+ this.opts.onChange(this.opts.fill);
822
+ }
823
+ }
824
+ });
825
+ new GradientEditor(this.body, {
826
+ stops: fill.stops,
827
+ onChange: (stops) => {
828
+ if (this.opts.fill.type === "linear-gradient") {
829
+ this.opts.fill.stops = stops;
830
+ this.opts.onChange(this.opts.fill);
831
+ }
832
+ }
833
+ });
834
+ } else if (fill.type === "radial-gradient") {
835
+ const row = document.createElement("div");
836
+ row.className = "atel-prop-row";
837
+ this.body.appendChild(row);
838
+ new NumberInput(row, {
839
+ label: "Center X",
840
+ value: parseUnit(fill.center.x),
841
+ step: 1,
842
+ suffix: "%",
843
+ onChange: (v) => {
844
+ if (this.opts.fill.type === "radial-gradient") {
845
+ this.opts.fill.center.x = `${v}%`;
846
+ this.opts.onChange(this.opts.fill);
847
+ }
848
+ }
849
+ });
850
+ new NumberInput(row, {
851
+ label: "Center Y",
852
+ value: parseUnit(fill.center.y),
853
+ step: 1,
854
+ suffix: "%",
855
+ onChange: (v) => {
856
+ if (this.opts.fill.type === "radial-gradient") {
857
+ this.opts.fill.center.y = `${v}%`;
858
+ this.opts.onChange(this.opts.fill);
859
+ }
860
+ }
861
+ });
862
+ new NumberInput(this.body, {
863
+ label: "Radius",
864
+ value: parseUnit(fill.radius),
865
+ step: 1,
866
+ suffix: "%",
867
+ onChange: (v) => {
868
+ if (this.opts.fill.type === "radial-gradient") {
869
+ this.opts.fill.radius = `${v}%`;
870
+ this.opts.onChange(this.opts.fill);
871
+ }
872
+ }
873
+ });
874
+ new GradientEditor(this.body, {
875
+ stops: fill.stops,
876
+ onChange: (stops) => {
877
+ if (this.opts.fill.type === "radial-gradient") {
878
+ this.opts.fill.stops = stops;
879
+ this.opts.onChange(this.opts.fill);
880
+ }
881
+ }
882
+ });
883
+ }
884
+ }
885
+ switchType(newType) {
886
+ const current = this.opts.fill;
887
+ let newFill;
888
+ if (newType === "solid") {
889
+ const color = extractFirstColor(current);
890
+ newFill = { type: "solid", color };
891
+ } else if (newType === "linear-gradient") {
892
+ const stops = extractStops(current);
893
+ const angle = current.type === "linear-gradient" ? current.angle : 0;
894
+ newFill = { type: "linear-gradient", angle, stops };
895
+ } else {
896
+ const stops = extractStops(current);
897
+ const center = current.type === "radial-gradient" ? current.center : { x: "50%", y: "50%" };
898
+ const radius = current.type === "radial-gradient" ? current.radius : "50%";
899
+ newFill = { type: "radial-gradient", center, radius, stops };
900
+ }
901
+ this.opts.fill = newFill;
902
+ this.opts.onChange(newFill);
903
+ this.buildBody();
904
+ }
905
+ };
906
+ function extractFirstColor(fill) {
907
+ if (fill.type === "solid") {
908
+ return typeof fill.color === "string" ? fill.color : "#888888";
909
+ }
910
+ const first = fill.stops[0];
911
+ return first && typeof first.color === "string" ? first.color : "#888888";
912
+ }
913
+ function extractStops(fill) {
914
+ if (fill.type === "solid") {
915
+ const color = typeof fill.color === "string" ? fill.color : "#888888";
916
+ return [
917
+ { offset: 0, color },
918
+ { offset: 1, color: "#ffffff" }
919
+ ];
920
+ }
921
+ return fill.stops;
922
+ }
923
+ function parseUnit(v) {
924
+ if (typeof v === "number") return v;
925
+ return parseFloat(v) || 0;
926
+ }
927
+
928
+ // src/controls/easing-curve-editor.ts
929
+ var import_atelier_math = require("@a-company/atelier-math");
930
+ var CANVAS_SIZE = 160;
931
+ var PAD = 20;
932
+ var PLOT_SIZE = CANVAS_SIZE - PAD * 2;
933
+ var HANDLE_R = 6;
934
+ var EasingCurveEditor = class {
935
+ constructor(parent, opts) {
936
+ this.opts = opts;
937
+ this.easing = opts.value ?? { type: "linear" };
938
+ this.el = document.createElement("div");
939
+ this.el.className = "atel-easing-curve";
940
+ this.canvas = document.createElement("canvas");
941
+ this.canvas.width = CANVAS_SIZE * 2;
942
+ this.canvas.height = CANVAS_SIZE * 2;
943
+ this.canvas.style.width = `${CANVAS_SIZE}px`;
944
+ this.canvas.style.height = `${CANVAS_SIZE}px`;
945
+ this.canvas.className = "atel-easing-curve__canvas";
946
+ this.ctx = this.canvas.getContext("2d");
947
+ this.ctx.scale(2, 2);
948
+ this.el.appendChild(this.canvas);
949
+ const info = document.createElement("div");
950
+ info.className = "atel-easing-curve__info";
951
+ info.textContent = this.getLabel();
952
+ this.el.appendChild(info);
953
+ parent.appendChild(this.el);
954
+ const signal = this.abortController.signal;
955
+ this.canvas.addEventListener("mousedown", (e) => this.onMouseDown(e), { signal });
956
+ this.canvas.addEventListener("mousemove", (e) => this.onMouseMove(e), { signal });
957
+ this.canvas.addEventListener("mouseup", () => this.onMouseUp(), { signal });
958
+ this.canvas.addEventListener("mouseleave", () => this.onMouseUp(), { signal });
959
+ this.draw();
960
+ }
961
+ el;
962
+ canvas;
963
+ ctx;
964
+ easing;
965
+ dragging = null;
966
+ abortController = new AbortController();
967
+ setValue(v) {
968
+ this.easing = v ?? { type: "linear" };
969
+ this.draw();
970
+ this.updateInfo();
971
+ }
972
+ destroy() {
973
+ this.abortController.abort();
974
+ }
975
+ getLabel() {
976
+ const e = this.easing;
977
+ if (typeof e === "string") return e;
978
+ if (e.type === "cubic-bezier") {
979
+ return `cubic-bezier(${e.x1.toFixed(2)}, ${e.y1.toFixed(2)}, ${e.x2.toFixed(2)}, ${e.y2.toFixed(2)})`;
980
+ }
981
+ if (e.type === "spring") {
982
+ return `spring(m:${e.mass ?? 1} k:${e.stiffness ?? 100} d:${e.damping ?? 10})`;
983
+ }
984
+ if (e.type === "step") {
985
+ return `steps(${e.steps}, ${e.position ?? "end"})`;
986
+ }
987
+ return e.type;
988
+ }
989
+ updateInfo() {
990
+ const info = this.el.querySelector(".atel-easing-curve__info");
991
+ if (info) info.textContent = this.getLabel();
992
+ }
993
+ getEasingFunction() {
994
+ const e = this.easing;
995
+ if (typeof e === "string") {
996
+ switch (e) {
997
+ case "ease-in":
998
+ return import_atelier_math.easeIn;
999
+ case "ease-out":
1000
+ return import_atelier_math.easeOut;
1001
+ case "ease-in-out":
1002
+ return import_atelier_math.easeInOut;
1003
+ default:
1004
+ return import_atelier_math.linear;
1005
+ }
1006
+ }
1007
+ switch (e.type) {
1008
+ case "linear":
1009
+ return import_atelier_math.linear;
1010
+ case "cubic-bezier":
1011
+ return (0, import_atelier_math.cubicBezier)(e.x1, e.y1, e.x2, e.y2);
1012
+ case "spring":
1013
+ return this.buildSpringFn(e);
1014
+ case "step":
1015
+ return (0, import_atelier_math.step)(e.steps, e.position);
1016
+ default:
1017
+ return import_atelier_math.linear;
1018
+ }
1019
+ }
1020
+ buildSpringFn(s) {
1021
+ const mass = s.mass ?? 1;
1022
+ const stiffness = s.stiffness ?? 100;
1023
+ const damping = s.damping ?? 10;
1024
+ const w0 = Math.sqrt(stiffness / mass);
1025
+ const zeta = damping / (2 * Math.sqrt(stiffness * mass));
1026
+ return (t) => {
1027
+ if (t <= 0) return 0;
1028
+ if (t >= 1) return 1;
1029
+ const scaledT = t * 3;
1030
+ if (zeta >= 1) {
1031
+ return 1 - Math.exp(-w0 * scaledT) * (1 + w0 * scaledT);
1032
+ }
1033
+ const wd = w0 * Math.sqrt(1 - zeta * zeta);
1034
+ return 1 - Math.exp(-zeta * w0 * scaledT) * (Math.cos(wd * scaledT) + zeta * w0 / wd * Math.sin(wd * scaledT));
1035
+ };
1036
+ }
1037
+ // ── Drawing ──────────────────────────────────────────────
1038
+ draw() {
1039
+ const ctx = this.ctx;
1040
+ const w = CANVAS_SIZE;
1041
+ const h = CANVAS_SIZE;
1042
+ ctx.clearRect(0, 0, w, h);
1043
+ ctx.fillStyle = "rgba(0,0,0,0.3)";
1044
+ ctx.fillRect(PAD, PAD, PLOT_SIZE, PLOT_SIZE);
1045
+ ctx.strokeStyle = "rgba(255,255,255,0.08)";
1046
+ ctx.lineWidth = 1;
1047
+ for (let i = 1; i < 4; i++) {
1048
+ const pos = PAD + PLOT_SIZE / 4 * i;
1049
+ ctx.beginPath();
1050
+ ctx.moveTo(pos, PAD);
1051
+ ctx.lineTo(pos, PAD + PLOT_SIZE);
1052
+ ctx.stroke();
1053
+ ctx.beginPath();
1054
+ ctx.moveTo(PAD, pos);
1055
+ ctx.lineTo(PAD + PLOT_SIZE, pos);
1056
+ ctx.stroke();
1057
+ }
1058
+ ctx.strokeStyle = "rgba(255,255,255,0.15)";
1059
+ ctx.setLineDash([4, 4]);
1060
+ ctx.beginPath();
1061
+ ctx.moveTo(PAD, PAD + PLOT_SIZE);
1062
+ ctx.lineTo(PAD + PLOT_SIZE, PAD);
1063
+ ctx.stroke();
1064
+ ctx.setLineDash([]);
1065
+ const fn = this.getEasingFunction();
1066
+ ctx.strokeStyle = "#58a6ff";
1067
+ ctx.lineWidth = 2;
1068
+ ctx.beginPath();
1069
+ const samples = 100;
1070
+ for (let i = 0; i <= samples; i++) {
1071
+ const t = i / samples;
1072
+ const v = fn(t);
1073
+ const x = PAD + t * PLOT_SIZE;
1074
+ const y = PAD + PLOT_SIZE - v * PLOT_SIZE;
1075
+ if (i === 0) ctx.moveTo(x, y);
1076
+ else ctx.lineTo(x, y);
1077
+ }
1078
+ ctx.stroke();
1079
+ const e = this.easing;
1080
+ if (typeof e !== "string" && e.type === "cubic-bezier") {
1081
+ this.drawBezierHandles(e);
1082
+ }
1083
+ ctx.fillStyle = "rgba(255,255,255,0.3)";
1084
+ ctx.font = "9px monospace";
1085
+ ctx.textAlign = "center";
1086
+ ctx.fillText("0", PAD, PAD + PLOT_SIZE + 14);
1087
+ ctx.fillText("1", PAD + PLOT_SIZE, PAD + PLOT_SIZE + 14);
1088
+ ctx.textAlign = "right";
1089
+ ctx.fillText("0", PAD - 4, PAD + PLOT_SIZE + 3);
1090
+ ctx.fillText("1", PAD - 4, PAD + 3);
1091
+ }
1092
+ drawBezierHandles(e) {
1093
+ const ctx = this.ctx;
1094
+ const p0x = PAD;
1095
+ const p0y = PAD + PLOT_SIZE;
1096
+ const p3x = PAD + PLOT_SIZE;
1097
+ const p3y = PAD;
1098
+ const p1x = PAD + e.x1 * PLOT_SIZE;
1099
+ const p1y = PAD + PLOT_SIZE - e.y1 * PLOT_SIZE;
1100
+ const p2x = PAD + e.x2 * PLOT_SIZE;
1101
+ const p2y = PAD + PLOT_SIZE - e.y2 * PLOT_SIZE;
1102
+ ctx.strokeStyle = "rgba(255,255,255,0.3)";
1103
+ ctx.lineWidth = 1;
1104
+ ctx.beginPath();
1105
+ ctx.moveTo(p0x, p0y);
1106
+ ctx.lineTo(p1x, p1y);
1107
+ ctx.stroke();
1108
+ ctx.beginPath();
1109
+ ctx.moveTo(p3x, p3y);
1110
+ ctx.lineTo(p2x, p2y);
1111
+ ctx.stroke();
1112
+ ctx.fillStyle = "#ff6b6b";
1113
+ ctx.beginPath();
1114
+ ctx.arc(p1x, p1y, HANDLE_R, 0, Math.PI * 2);
1115
+ ctx.fill();
1116
+ ctx.strokeStyle = "rgba(255,255,255,0.6)";
1117
+ ctx.lineWidth = 1.5;
1118
+ ctx.stroke();
1119
+ ctx.fillStyle = "#51cf66";
1120
+ ctx.beginPath();
1121
+ ctx.arc(p2x, p2y, HANDLE_R, 0, Math.PI * 2);
1122
+ ctx.fill();
1123
+ ctx.strokeStyle = "rgba(255,255,255,0.6)";
1124
+ ctx.lineWidth = 1.5;
1125
+ ctx.stroke();
1126
+ }
1127
+ // ── Interaction ──────────────────────────────────────────
1128
+ canvasToPlot(e) {
1129
+ const rect = this.canvas.getBoundingClientRect();
1130
+ const px = e.clientX - rect.left;
1131
+ const py = e.clientY - rect.top;
1132
+ const x = (px - PAD) / PLOT_SIZE;
1133
+ const y = 1 - (py - PAD) / PLOT_SIZE;
1134
+ return { x, y };
1135
+ }
1136
+ onMouseDown(e) {
1137
+ const easing = this.easing;
1138
+ if (typeof easing === "string" || easing.type !== "cubic-bezier") return;
1139
+ const { x, y } = this.canvasToPlot(e);
1140
+ const dist1 = Math.hypot(x - easing.x1, y - easing.y1);
1141
+ const dist2 = Math.hypot(x - easing.x2, y - easing.y2);
1142
+ const threshold = HANDLE_R / PLOT_SIZE + 0.03;
1143
+ if (dist1 < threshold && dist1 <= dist2) {
1144
+ this.dragging = "p1";
1145
+ this.canvas.style.cursor = "grabbing";
1146
+ } else if (dist2 < threshold) {
1147
+ this.dragging = "p2";
1148
+ this.canvas.style.cursor = "grabbing";
1149
+ }
1150
+ }
1151
+ onMouseMove(e) {
1152
+ const easing = this.easing;
1153
+ if (typeof easing === "string" || easing.type !== "cubic-bezier") return;
1154
+ if (!this.dragging) {
1155
+ const { x: x2, y: y2 } = this.canvasToPlot(e);
1156
+ const dist1 = Math.hypot(x2 - easing.x1, y2 - easing.y1);
1157
+ const dist2 = Math.hypot(x2 - easing.x2, y2 - easing.y2);
1158
+ const threshold = HANDLE_R / PLOT_SIZE + 0.03;
1159
+ this.canvas.style.cursor = dist1 < threshold || dist2 < threshold ? "grab" : "";
1160
+ return;
1161
+ }
1162
+ const { x, y } = this.canvasToPlot(e);
1163
+ const cx = Math.max(0, Math.min(1, x));
1164
+ const cy = Math.max(-0.5, Math.min(1.5, y));
1165
+ if (this.dragging === "p1") {
1166
+ easing.x1 = Math.round(cx * 100) / 100;
1167
+ easing.y1 = Math.round(cy * 100) / 100;
1168
+ } else {
1169
+ easing.x2 = Math.round(cx * 100) / 100;
1170
+ easing.y2 = Math.round(cy * 100) / 100;
1171
+ }
1172
+ this.draw();
1173
+ this.updateInfo();
1174
+ this.opts.onChange(easing);
1175
+ }
1176
+ onMouseUp() {
1177
+ if (this.dragging) {
1178
+ this.dragging = null;
1179
+ this.canvas.style.cursor = "";
1180
+ }
1181
+ }
1182
+ };
1183
+
1184
+ // src/controls/easing-picker.ts
1185
+ var PRESET_OPTIONS = [
1186
+ { label: "Linear", value: "linear" },
1187
+ { label: "Ease In", value: "ease-in" },
1188
+ { label: "Ease Out", value: "ease-out" },
1189
+ { label: "Ease In-Out", value: "ease-in-out" },
1190
+ { label: "Spring", value: "spring" },
1191
+ { label: "Cubic Bezier", value: "cubic-bezier" },
1192
+ { label: "Step", value: "step" }
1193
+ ];
1194
+ var EasingPicker = class {
1195
+ constructor(parent, opts) {
1196
+ this.opts = opts;
1197
+ this.currentEasing = opts.value;
1198
+ this.el = document.createElement("div");
1199
+ this.el.className = "atel-easing-picker";
1200
+ const label = document.createElement("label");
1201
+ label.className = "atel-control-label";
1202
+ label.textContent = opts.label;
1203
+ this.select = document.createElement("select");
1204
+ this.select.className = "atel-input";
1205
+ for (const o of PRESET_OPTIONS) {
1206
+ const opt = document.createElement("option");
1207
+ opt.value = o.value;
1208
+ opt.textContent = o.label;
1209
+ this.select.appendChild(opt);
1210
+ }
1211
+ this.configContainer = document.createElement("div");
1212
+ this.configContainer.className = "atel-easing-picker__config";
1213
+ this.el.appendChild(label);
1214
+ this.el.appendChild(this.select);
1215
+ this.el.appendChild(this.configContainer);
1216
+ parent.appendChild(this.el);
1217
+ this.syncFromValue(opts.value);
1218
+ this.select.addEventListener("change", () => {
1219
+ this.emitChange();
1220
+ });
1221
+ }
1222
+ el;
1223
+ select;
1224
+ configContainer;
1225
+ curveEditor = null;
1226
+ currentEasing;
1227
+ syncFromValue(v) {
1228
+ this.currentEasing = v;
1229
+ if (!v) {
1230
+ this.select.value = "linear";
1231
+ } else if (typeof v === "string") {
1232
+ this.select.value = v;
1233
+ } else {
1234
+ this.select.value = v.type;
1235
+ }
1236
+ this.updateConfig();
1237
+ }
1238
+ updateConfig() {
1239
+ this.configContainer.innerHTML = "";
1240
+ this.curveEditor?.destroy();
1241
+ this.curveEditor = null;
1242
+ const easing = this.currentEasing;
1243
+ this.curveEditor = new EasingCurveEditor(this.configContainer, {
1244
+ value: easing,
1245
+ onChange: (v) => {
1246
+ this.currentEasing = v;
1247
+ this.opts.onChange(v);
1248
+ }
1249
+ });
1250
+ if (typeof easing !== "string" && easing) {
1251
+ if (easing.type === "cubic-bezier") {
1252
+ this.buildBezierConfig(easing);
1253
+ } else if (easing.type === "spring") {
1254
+ this.buildSpringConfig(easing);
1255
+ } else if (easing.type === "step") {
1256
+ this.buildStepConfig(easing);
1257
+ }
1258
+ }
1259
+ }
1260
+ buildBezierConfig(easing) {
1261
+ const row = document.createElement("div");
1262
+ row.className = "atel-easing-picker__params";
1263
+ new NumberInput(row, {
1264
+ label: "x1",
1265
+ value: easing.x1,
1266
+ step: 0.01,
1267
+ min: 0,
1268
+ max: 1,
1269
+ onChange: (v) => {
1270
+ easing.x1 = v;
1271
+ this.curveEditor?.setValue(easing);
1272
+ this.opts.onChange(easing);
1273
+ }
1274
+ });
1275
+ new NumberInput(row, {
1276
+ label: "y1",
1277
+ value: easing.y1,
1278
+ step: 0.01,
1279
+ onChange: (v) => {
1280
+ easing.y1 = v;
1281
+ this.curveEditor?.setValue(easing);
1282
+ this.opts.onChange(easing);
1283
+ }
1284
+ });
1285
+ new NumberInput(row, {
1286
+ label: "x2",
1287
+ value: easing.x2,
1288
+ step: 0.01,
1289
+ min: 0,
1290
+ max: 1,
1291
+ onChange: (v) => {
1292
+ easing.x2 = v;
1293
+ this.curveEditor?.setValue(easing);
1294
+ this.opts.onChange(easing);
1295
+ }
1296
+ });
1297
+ new NumberInput(row, {
1298
+ label: "y2",
1299
+ value: easing.y2,
1300
+ step: 0.01,
1301
+ onChange: (v) => {
1302
+ easing.y2 = v;
1303
+ this.curveEditor?.setValue(easing);
1304
+ this.opts.onChange(easing);
1305
+ }
1306
+ });
1307
+ this.configContainer.appendChild(row);
1308
+ }
1309
+ buildSpringConfig(easing) {
1310
+ const row = document.createElement("div");
1311
+ row.className = "atel-easing-picker__params";
1312
+ new NumberInput(row, {
1313
+ label: "Mass",
1314
+ value: easing.mass ?? 1,
1315
+ step: 0.1,
1316
+ min: 0.1,
1317
+ onChange: (v) => {
1318
+ easing.mass = v;
1319
+ this.curveEditor?.setValue(easing);
1320
+ this.opts.onChange(easing);
1321
+ }
1322
+ });
1323
+ new NumberInput(row, {
1324
+ label: "Stiffness",
1325
+ value: easing.stiffness ?? 100,
1326
+ step: 10,
1327
+ min: 1,
1328
+ onChange: (v) => {
1329
+ easing.stiffness = v;
1330
+ this.curveEditor?.setValue(easing);
1331
+ this.opts.onChange(easing);
1332
+ }
1333
+ });
1334
+ new NumberInput(row, {
1335
+ label: "Damping",
1336
+ value: easing.damping ?? 10,
1337
+ step: 1,
1338
+ min: 0,
1339
+ onChange: (v) => {
1340
+ easing.damping = v;
1341
+ this.curveEditor?.setValue(easing);
1342
+ this.opts.onChange(easing);
1343
+ }
1344
+ });
1345
+ this.configContainer.appendChild(row);
1346
+ }
1347
+ buildStepConfig(easing) {
1348
+ const row = document.createElement("div");
1349
+ row.className = "atel-easing-picker__params";
1350
+ new NumberInput(row, {
1351
+ label: "Steps",
1352
+ value: easing.steps,
1353
+ step: 1,
1354
+ min: 1,
1355
+ max: 20,
1356
+ onChange: (v) => {
1357
+ easing.steps = Math.round(v);
1358
+ this.curveEditor?.setValue(easing);
1359
+ this.opts.onChange(easing);
1360
+ }
1361
+ });
1362
+ new SelectInput(row, {
1363
+ label: "Position",
1364
+ value: easing.position ?? "end",
1365
+ options: [
1366
+ { label: "End", value: "end" },
1367
+ { label: "Start", value: "start" }
1368
+ ],
1369
+ onChange: (v) => {
1370
+ easing.position = v;
1371
+ this.curveEditor?.setValue(easing);
1372
+ this.opts.onChange(easing);
1373
+ }
1374
+ });
1375
+ this.configContainer.appendChild(row);
1376
+ }
1377
+ emitChange() {
1378
+ const val = this.select.value;
1379
+ let easing;
1380
+ if (val === "linear") {
1381
+ easing = { type: "linear" };
1382
+ } else if (val === "ease-in" || val === "ease-out" || val === "ease-in-out") {
1383
+ easing = val;
1384
+ } else if (val === "spring") {
1385
+ easing = { type: "spring", mass: 1, stiffness: 100, damping: 10 };
1386
+ } else if (val === "cubic-bezier") {
1387
+ easing = { type: "cubic-bezier", x1: 0.25, y1: 0.1, x2: 0.25, y2: 1 };
1388
+ } else if (val === "step") {
1389
+ easing = { type: "step", steps: 5, position: "end" };
1390
+ }
1391
+ this.currentEasing = easing;
1392
+ this.updateConfig();
1393
+ this.opts.onChange(easing);
1394
+ }
1395
+ setValue(v) {
1396
+ this.syncFromValue(v);
1397
+ }
1398
+ };
1399
+
1400
+ // src/controls/range-slider.ts
1401
+ var RangeSlider = class {
1402
+ constructor(parent, opts) {
1403
+ this.opts = opts;
1404
+ this.el = document.createElement("div");
1405
+ this.el.className = "atel-range-slider-control";
1406
+ const label = document.createElement("label");
1407
+ label.className = "atel-control-label";
1408
+ label.textContent = opts.label;
1409
+ this.el.appendChild(label);
1410
+ const slider = document.createElement("div");
1411
+ slider.className = "atel-range-slider";
1412
+ const track = document.createElement("div");
1413
+ track.className = "atel-range-slider__track";
1414
+ slider.appendChild(track);
1415
+ this.fill = document.createElement("div");
1416
+ this.fill.className = "atel-range-slider__fill";
1417
+ slider.appendChild(this.fill);
1418
+ const step2 = opts.step ?? 1;
1419
+ this.inputLow = document.createElement("input");
1420
+ this.inputLow.type = "range";
1421
+ this.inputLow.min = String(opts.min);
1422
+ this.inputLow.max = String(opts.max);
1423
+ this.inputLow.step = String(step2);
1424
+ this.inputLow.value = String(opts.valueLow);
1425
+ this.inputHigh = document.createElement("input");
1426
+ this.inputHigh.type = "range";
1427
+ this.inputHigh.min = String(opts.min);
1428
+ this.inputHigh.max = String(opts.max);
1429
+ this.inputHigh.step = String(step2);
1430
+ this.inputHigh.value = String(opts.valueHigh);
1431
+ slider.appendChild(this.inputLow);
1432
+ slider.appendChild(this.inputHigh);
1433
+ this.el.appendChild(slider);
1434
+ parent.appendChild(this.el);
1435
+ this.updateFill();
1436
+ this.inputLow.addEventListener("input", () => {
1437
+ let low = parseInt(this.inputLow.value, 10);
1438
+ const high = parseInt(this.inputHigh.value, 10);
1439
+ if (low > high) {
1440
+ low = high;
1441
+ this.inputLow.value = String(low);
1442
+ }
1443
+ this.updateFill();
1444
+ opts.onChangeLow(low);
1445
+ });
1446
+ this.inputHigh.addEventListener("input", () => {
1447
+ const low = parseInt(this.inputLow.value, 10);
1448
+ let high = parseInt(this.inputHigh.value, 10);
1449
+ if (high < low) {
1450
+ high = low;
1451
+ this.inputHigh.value = String(high);
1452
+ }
1453
+ this.updateFill();
1454
+ opts.onChangeHigh(high);
1455
+ });
1456
+ }
1457
+ el;
1458
+ inputLow;
1459
+ inputHigh;
1460
+ fill;
1461
+ setValue(low, high) {
1462
+ this.inputLow.value = String(low);
1463
+ this.inputHigh.value = String(high);
1464
+ this.updateFill();
1465
+ }
1466
+ updateFill() {
1467
+ const min = this.opts.min;
1468
+ const max = this.opts.max;
1469
+ const range = max - min || 1;
1470
+ const low = parseInt(this.inputLow.value, 10);
1471
+ const high = parseInt(this.inputHigh.value, 10);
1472
+ const left = (low - min) / range * 100;
1473
+ const right = (max - high) / range * 100;
1474
+ this.fill.style.left = `${left}%`;
1475
+ this.fill.style.right = `${right}%`;
1476
+ }
1477
+ };
1478
+
1479
+ // src/panels/delta-panel.ts
1480
+ var ANIMATABLE_PROPERTIES = [
1481
+ { label: "frame.x", value: "frame.x" },
1482
+ { label: "frame.y", value: "frame.y" },
1483
+ { label: "bounds.width", value: "bounds.width" },
1484
+ { label: "bounds.height", value: "bounds.height" },
1485
+ { label: "opacity", value: "opacity" },
1486
+ { label: "rotation", value: "rotation" },
1487
+ { label: "scale.x", value: "scale.x" },
1488
+ { label: "scale.y", value: "scale.y" },
1489
+ { label: "anchorPoint.x", value: "anchorPoint.x" },
1490
+ { label: "anchorPoint.y", value: "anchorPoint.y" },
1491
+ { label: "visual.fill.color", value: "visual.fill.color" },
1492
+ { label: "visual.stroke.color", value: "visual.stroke.color" },
1493
+ { label: "visual.stroke.width", value: "visual.stroke.width" },
1494
+ { label: "visual.stroke.start", value: "visual.stroke.start" },
1495
+ { label: "visual.stroke.end", value: "visual.stroke.end" },
1496
+ { label: "visual.shape.cornerRadius", value: "visual.shape.cornerRadius" },
1497
+ { label: "visual.style.fontSize", value: "visual.style.fontSize" },
1498
+ { label: "visual.style.color", value: "visual.style.color" },
1499
+ { label: "shadow.blur", value: "shadow.blur" },
1500
+ { label: "shadow.color", value: "shadow.color" },
1501
+ { label: "shadow.offsetX", value: "shadow.offsetX" },
1502
+ { label: "shadow.offsetY", value: "shadow.offsetY" }
1503
+ ];
1504
+ var DeltaPanel = class {
1505
+ constructor(parent, opts) {
1506
+ this.opts = opts;
1507
+ this.el = document.createElement("div");
1508
+ this.el.className = "atel-prop-section";
1509
+ this.deltas = opts.state.deltas.filter((d) => d.layer === opts.layerId);
1510
+ this.title = document.createElement("div");
1511
+ this.title.className = "atel-prop-section__title atel-prop-section__title--deltas";
1512
+ const titleText = document.createElement("span");
1513
+ titleText.textContent = `Deltas (${this.deltas.length})`;
1514
+ this.title.appendChild(titleText);
1515
+ const sortBtn = document.createElement("button");
1516
+ sortBtn.className = "atel-delta-sort-btn";
1517
+ sortBtn.textContent = "\u2195 Sort";
1518
+ sortBtn.title = "Sort by start frame";
1519
+ sortBtn.addEventListener("click", () => this.sortByStartFrame());
1520
+ this.title.appendChild(sortBtn);
1521
+ this.el.appendChild(this.title);
1522
+ this.list = document.createElement("div");
1523
+ this.list.className = "atel-delta-list";
1524
+ this.el.appendChild(this.list);
1525
+ enableDragReorder({
1526
+ list: this.list,
1527
+ itemClass: "atel-delta-item",
1528
+ onReorder: (from, to) => this.reorderDelta(from, to)
1529
+ });
1530
+ this.renderList();
1531
+ const actions = document.createElement("div");
1532
+ actions.className = "atel-delta-actions";
1533
+ const addBtn = document.createElement("button");
1534
+ addBtn.textContent = "+ Add Delta";
1535
+ addBtn.addEventListener("click", () => this.addDelta());
1536
+ actions.appendChild(addBtn);
1537
+ this.el.appendChild(actions);
1538
+ parent.appendChild(this.el);
1539
+ }
1540
+ el;
1541
+ deltas;
1542
+ list;
1543
+ title;
1544
+ expandedIndex = null;
1545
+ updateTitleCount() {
1546
+ const span = this.title.firstElementChild;
1547
+ if (span) span.textContent = `Deltas (${this.deltas.length})`;
1548
+ }
1549
+ renderList() {
1550
+ this.list.innerHTML = "";
1551
+ for (let i = 0; i < this.deltas.length; i++) {
1552
+ this.list.appendChild(this.renderDelta(this.deltas[i], i));
1553
+ }
1554
+ }
1555
+ renderDelta(delta, index) {
1556
+ const expanded = this.expandedIndex === index;
1557
+ const item = document.createElement("div");
1558
+ item.className = "atel-delta-item" + (expanded ? " atel-delta-item--expanded" : "");
1559
+ const header = document.createElement("div");
1560
+ header.className = "atel-delta-item__header";
1561
+ const handle = document.createElement("span");
1562
+ handle.className = "atel-drag-handle";
1563
+ handle.textContent = "\u2630";
1564
+ const prop = document.createElement("span");
1565
+ prop.className = "atel-delta-item__prop";
1566
+ prop.textContent = delta.property;
1567
+ const range = document.createElement("span");
1568
+ range.className = "atel-delta-item__range";
1569
+ range.textContent = `[${delta.range[0]}\u2013${delta.range[1]}]`;
1570
+ const removeBtn = document.createElement("button");
1571
+ removeBtn.className = "atel-delta-item__remove";
1572
+ removeBtn.textContent = "\u2715";
1573
+ removeBtn.addEventListener("click", (e) => {
1574
+ e.stopPropagation();
1575
+ this.removeDelta(delta);
1576
+ });
1577
+ header.addEventListener("click", () => {
1578
+ this.expandedIndex = this.expandedIndex === index ? null : index;
1579
+ this.renderList();
1580
+ });
1581
+ header.appendChild(handle);
1582
+ if (delta.name) {
1583
+ const nameSpan = document.createElement("span");
1584
+ nameSpan.className = "atel-delta-item__name";
1585
+ nameSpan.textContent = delta.name;
1586
+ header.appendChild(nameSpan);
1587
+ }
1588
+ header.appendChild(prop);
1589
+ header.appendChild(range);
1590
+ header.appendChild(removeBtn);
1591
+ item.appendChild(header);
1592
+ if (expanded) {
1593
+ item.appendChild(this.buildEditor(delta));
1594
+ } else {
1595
+ const detail = document.createElement("div");
1596
+ detail.className = "atel-delta-item__detail";
1597
+ const easingLabel = formatEasing(delta.easing);
1598
+ detail.textContent = `${formatValue2(delta.from)} \u2192 ${formatValue2(delta.to)} ${easingLabel}`;
1599
+ item.appendChild(detail);
1600
+ const duration = this.opts.state.duration || 1;
1601
+ const bar = document.createElement("div");
1602
+ bar.className = "atel-delta-item__minibar";
1603
+ const fill = document.createElement("div");
1604
+ fill.className = "atel-delta-item__minibar-fill";
1605
+ const left = delta.range[0] / duration * 100;
1606
+ const right = (duration - delta.range[1]) / duration * 100;
1607
+ fill.style.left = `${left}%`;
1608
+ fill.style.right = `${right}%`;
1609
+ bar.appendChild(fill);
1610
+ item.appendChild(bar);
1611
+ }
1612
+ return item;
1613
+ }
1614
+ buildEditor(delta) {
1615
+ const editor = document.createElement("div");
1616
+ editor.className = "atel-delta-item__editor";
1617
+ const notify = () => this.opts.onDeltaChange();
1618
+ const nameRow = document.createElement("div");
1619
+ nameRow.className = "atel-delta-item__editor-row";
1620
+ const nameLabel = document.createElement("label");
1621
+ nameLabel.className = "atel-control-label";
1622
+ nameLabel.textContent = "Name";
1623
+ const nameInput = document.createElement("input");
1624
+ nameInput.type = "text";
1625
+ nameInput.className = "atel-input";
1626
+ nameInput.placeholder = "unnamed";
1627
+ nameInput.value = delta.name ?? "";
1628
+ nameInput.addEventListener("input", () => {
1629
+ const v = nameInput.value.trim();
1630
+ delta.name = v || void 0;
1631
+ this.renderList();
1632
+ notify();
1633
+ });
1634
+ nameRow.appendChild(nameLabel);
1635
+ nameRow.appendChild(nameInput);
1636
+ editor.appendChild(nameRow);
1637
+ const propRow = document.createElement("div");
1638
+ propRow.className = "atel-delta-item__editor-row";
1639
+ new SelectInput(propRow, {
1640
+ label: "Property",
1641
+ value: delta.property,
1642
+ options: ANIMATABLE_PROPERTIES,
1643
+ onChange: (v) => {
1644
+ const prev = delta.property;
1645
+ delta.property = v;
1646
+ if (prev !== delta.property) {
1647
+ const defaults = getDefaultValues(delta.property);
1648
+ delta.from = defaults.from;
1649
+ delta.to = defaults.to;
1650
+ this.renderList();
1651
+ }
1652
+ notify();
1653
+ }
1654
+ });
1655
+ editor.appendChild(propRow);
1656
+ const fromRow = document.createElement("div");
1657
+ fromRow.className = "atel-delta-item__editor-row";
1658
+ this.buildControlForProperty(fromRow, delta.property, "From", delta.from, (v) => {
1659
+ delta.from = v;
1660
+ notify();
1661
+ });
1662
+ editor.appendChild(fromRow);
1663
+ const toRow = document.createElement("div");
1664
+ toRow.className = "atel-delta-item__editor-row";
1665
+ this.buildControlForProperty(toRow, delta.property, "To", delta.to, (v) => {
1666
+ delta.to = v;
1667
+ notify();
1668
+ });
1669
+ editor.appendChild(toRow);
1670
+ const easingRow = document.createElement("div");
1671
+ easingRow.className = "atel-delta-item__editor-row";
1672
+ new EasingPicker(easingRow, {
1673
+ label: "Easing",
1674
+ value: delta.easing,
1675
+ onChange: (v) => {
1676
+ delta.easing = v;
1677
+ notify();
1678
+ }
1679
+ });
1680
+ editor.appendChild(easingRow);
1681
+ const sliderRow = document.createElement("div");
1682
+ sliderRow.className = "atel-delta-item__editor-row";
1683
+ const rangeSlider = new RangeSlider(sliderRow, {
1684
+ label: "Frame Range",
1685
+ min: 0,
1686
+ max: Math.max(1, this.opts.state.duration),
1687
+ step: 1,
1688
+ valueLow: delta.range[0],
1689
+ valueHigh: delta.range[1],
1690
+ onChangeLow: (v) => {
1691
+ delta.range[0] = v;
1692
+ startInput.setValue(v);
1693
+ notify();
1694
+ },
1695
+ onChangeHigh: (v) => {
1696
+ delta.range[1] = v;
1697
+ endInput.setValue(v);
1698
+ notify();
1699
+ }
1700
+ });
1701
+ editor.appendChild(sliderRow);
1702
+ const rangeRow = document.createElement("div");
1703
+ rangeRow.className = "atel-delta-item__editor-row atel-delta-item__editor-row--range";
1704
+ const startInput = new NumberInput(rangeRow, {
1705
+ label: "Start Frame",
1706
+ value: delta.range[0],
1707
+ step: 1,
1708
+ min: 0,
1709
+ onChange: (v) => {
1710
+ delta.range[0] = Math.round(v);
1711
+ rangeSlider.setValue(delta.range[0], delta.range[1]);
1712
+ notify();
1713
+ }
1714
+ });
1715
+ const endInput = new NumberInput(rangeRow, {
1716
+ label: "End Frame",
1717
+ value: delta.range[1],
1718
+ step: 1,
1719
+ min: 0,
1720
+ onChange: (v) => {
1721
+ delta.range[1] = Math.round(v);
1722
+ rangeSlider.setValue(delta.range[0], delta.range[1]);
1723
+ notify();
1724
+ }
1725
+ });
1726
+ editor.appendChild(rangeRow);
1727
+ return editor;
1728
+ }
1729
+ buildControlForProperty(parent, property, label, value, onChange) {
1730
+ if (isColorProperty(property)) {
1731
+ new ColorPicker(parent, {
1732
+ label,
1733
+ value: typeof value === "string" ? value : "#000000",
1734
+ onChange
1735
+ });
1736
+ } else if (property === "opacity" || property === "visual.stroke.start" || property === "visual.stroke.end") {
1737
+ new SliderInput(parent, {
1738
+ label,
1739
+ value: typeof value === "number" ? value : 0,
1740
+ min: 0,
1741
+ max: 1,
1742
+ step: 0.01,
1743
+ onChange
1744
+ });
1745
+ } else {
1746
+ const config = getNumberConfig(property);
1747
+ new NumberInput(parent, {
1748
+ label,
1749
+ value: typeof value === "number" ? value : 0,
1750
+ ...config,
1751
+ onChange: (v) => onChange(v)
1752
+ });
1753
+ }
1754
+ }
1755
+ reorderDelta(from, to) {
1756
+ const allDeltas = this.opts.state.deltas;
1757
+ const layerId = this.opts.layerId;
1758
+ const absIndices = [];
1759
+ for (let i = 0; i < allDeltas.length; i++) {
1760
+ if (allDeltas[i].layer === layerId) absIndices.push(i);
1761
+ }
1762
+ const absFrom = absIndices[from];
1763
+ const [moved] = allDeltas.splice(absFrom, 1);
1764
+ const newAbsIndices = [];
1765
+ for (let i = 0; i < allDeltas.length; i++) {
1766
+ if (allDeltas[i].layer === layerId) newAbsIndices.push(i);
1767
+ }
1768
+ let insertAt;
1769
+ if (to >= newAbsIndices.length) {
1770
+ insertAt = (newAbsIndices.length > 0 ? newAbsIndices[newAbsIndices.length - 1] : absFrom) + 1;
1771
+ } else {
1772
+ insertAt = newAbsIndices[to];
1773
+ }
1774
+ allDeltas.splice(insertAt, 0, moved);
1775
+ this.deltas = allDeltas.filter((d) => d.layer === layerId);
1776
+ this.expandedIndex = null;
1777
+ this.renderList();
1778
+ this.opts.onDeltaChange();
1779
+ }
1780
+ sortByStartFrame() {
1781
+ this.deltas.sort((a, b) => a.range[0] - b.range[0]);
1782
+ const allDeltas = this.opts.state.deltas;
1783
+ const layerId = this.opts.layerId;
1784
+ const otherDeltas = allDeltas.filter((d) => d.layer !== layerId);
1785
+ let insertAt = allDeltas.findIndex((d) => d.layer === layerId);
1786
+ if (insertAt === -1) insertAt = allDeltas.length;
1787
+ allDeltas.length = 0;
1788
+ for (let i = 0; i < otherDeltas.length; i++) {
1789
+ allDeltas.push(otherDeltas[i]);
1790
+ }
1791
+ for (let i = this.deltas.length - 1; i >= 0; i--) {
1792
+ allDeltas.splice(Math.min(insertAt, allDeltas.length), 0, this.deltas[i]);
1793
+ }
1794
+ this.expandedIndex = null;
1795
+ this.renderList();
1796
+ this.opts.onDeltaChange();
1797
+ }
1798
+ addDelta() {
1799
+ const delta = {
1800
+ layer: this.opts.layerId,
1801
+ property: "opacity",
1802
+ range: [0, Math.max(1, this.opts.state.duration - 1)],
1803
+ from: 0,
1804
+ to: 1,
1805
+ easing: "ease-out"
1806
+ };
1807
+ this.opts.state.deltas.push(delta);
1808
+ this.deltas = this.opts.state.deltas.filter((d) => d.layer === this.opts.layerId);
1809
+ this.expandedIndex = this.deltas.length - 1;
1810
+ this.updateTitleCount();
1811
+ this.renderList();
1812
+ this.opts.onDeltaChange();
1813
+ }
1814
+ removeDelta(delta) {
1815
+ const idx = this.opts.state.deltas.indexOf(delta);
1816
+ if (idx !== -1) {
1817
+ this.opts.state.deltas.splice(idx, 1);
1818
+ this.deltas = this.opts.state.deltas.filter((d) => d.layer === this.opts.layerId);
1819
+ this.expandedIndex = null;
1820
+ this.updateTitleCount();
1821
+ this.renderList();
1822
+ this.opts.onDeltaChange();
1823
+ }
1824
+ }
1825
+ };
1826
+ function isColorProperty(p) {
1827
+ return p === "visual.fill.color" || p === "visual.stroke.color" || p === "visual.style.color" || p === "shadow.color";
1828
+ }
1829
+ function getNumberConfig(p) {
1830
+ switch (p) {
1831
+ case "frame.x":
1832
+ case "frame.y":
1833
+ case "anchorPoint.x":
1834
+ case "anchorPoint.y":
1835
+ return { step: 1 };
1836
+ case "bounds.width":
1837
+ case "bounds.height":
1838
+ return { step: 1, min: 1 };
1839
+ case "rotation":
1840
+ return { step: 1, suffix: "\xB0" };
1841
+ case "scale.x":
1842
+ case "scale.y":
1843
+ return { step: 0.1 };
1844
+ case "visual.stroke.width":
1845
+ case "visual.shape.cornerRadius":
1846
+ return { step: 1, min: 0 };
1847
+ case "visual.style.fontSize":
1848
+ return { step: 1, min: 1 };
1849
+ case "shadow.blur":
1850
+ return { step: 1, min: 0, max: 50 };
1851
+ case "shadow.offsetX":
1852
+ case "shadow.offsetY":
1853
+ return { step: 1 };
1854
+ default:
1855
+ return { step: 1 };
1856
+ }
1857
+ }
1858
+ function getDefaultValues(p) {
1859
+ if (isColorProperty(p)) return { from: "#000000", to: "#ffffff" };
1860
+ if (p === "opacity" || p === "visual.stroke.start" || p === "visual.stroke.end") return { from: 0, to: 1 };
1861
+ if (p === "scale.x" || p === "scale.y") return { from: 1, to: 2 };
1862
+ if (p === "bounds.width" || p === "bounds.height") return { from: 100, to: 200 };
1863
+ if (p === "shadow.blur") return { from: 0, to: 30 };
1864
+ if (p === "shadow.offsetX" || p === "shadow.offsetY") return { from: 0, to: 10 };
1865
+ return { from: 0, to: 100 };
1866
+ }
1867
+ function formatValue2(v) {
1868
+ if (typeof v === "number") return v % 1 === 0 ? String(v) : v.toFixed(2);
1869
+ if (typeof v === "string") return v;
1870
+ return String(v);
1871
+ }
1872
+ function formatEasing(e) {
1873
+ if (!e) return "linear";
1874
+ if (typeof e === "string") return e;
1875
+ return e.type;
1876
+ }
1877
+
1878
+ // src/panels/property-panel.ts
1879
+ var PropertyPanel = class {
1880
+ constructor(parent, opts) {
1881
+ this.opts = opts;
1882
+ this.el = document.createElement("div");
1883
+ this.el.className = "atel-property-panel";
1884
+ const header = document.createElement("div");
1885
+ header.className = "atel-panel-header";
1886
+ header.textContent = "Properties";
1887
+ this.el.appendChild(header);
1888
+ this.scroll = document.createElement("div");
1889
+ this.scroll.className = "atel-property-scroll";
1890
+ this.el.appendChild(this.scroll);
1891
+ parent.appendChild(this.el);
1892
+ }
1893
+ el;
1894
+ scroll;
1895
+ deltaPanel = null;
1896
+ doc = null;
1897
+ stateName = "";
1898
+ layerId = null;
1899
+ loadDocument(doc, stateName) {
1900
+ this.doc = doc;
1901
+ this.stateName = stateName;
1902
+ }
1903
+ setStateName(name) {
1904
+ this.stateName = name;
1905
+ if (this.layerId) this.selectLayer(this.layerId);
1906
+ }
1907
+ selectLayer(layerId) {
1908
+ this.layerId = layerId;
1909
+ this.render();
1910
+ }
1911
+ refresh() {
1912
+ this.render();
1913
+ }
1914
+ render() {
1915
+ this.scroll.innerHTML = "";
1916
+ this.deltaPanel = null;
1917
+ if (!this.doc || !this.layerId) {
1918
+ const empty = document.createElement("div");
1919
+ empty.className = "atel-empty";
1920
+ empty.textContent = "Select a layer";
1921
+ this.scroll.appendChild(empty);
1922
+ return;
1923
+ }
1924
+ const layer = this.doc.layers.find((l) => l.id === this.layerId);
1925
+ if (!layer) return;
1926
+ this.addSection("Layer", () => {
1927
+ const row = this.makeRow(true);
1928
+ this.addTextInput(row, "ID", layer.id, (v) => {
1929
+ for (const state2 of Object.values(this.doc.states)) {
1930
+ for (const d of state2.deltas) {
1931
+ if (d.layer === layer.id) d.layer = v;
1932
+ }
1933
+ }
1934
+ layer.id = v;
1935
+ this.layerId = v;
1936
+ this.opts.onPropertyChange();
1937
+ });
1938
+ });
1939
+ this.addSection("Position", () => {
1940
+ const row = this.makeRow();
1941
+ new NumberInput(row, {
1942
+ label: "X",
1943
+ value: layer.frame.x,
1944
+ step: 1,
1945
+ onChange: (v) => {
1946
+ layer.frame.x = v;
1947
+ this.opts.onPropertyChange();
1948
+ }
1949
+ });
1950
+ new NumberInput(row, {
1951
+ label: "Y",
1952
+ value: layer.frame.y,
1953
+ step: 1,
1954
+ onChange: (v) => {
1955
+ layer.frame.y = v;
1956
+ this.opts.onPropertyChange();
1957
+ }
1958
+ });
1959
+ });
1960
+ this.addSection("Size", () => {
1961
+ const row = this.makeRow();
1962
+ new NumberInput(row, {
1963
+ label: "W",
1964
+ value: layer.bounds.width,
1965
+ step: 1,
1966
+ min: 1,
1967
+ onChange: (v) => {
1968
+ layer.bounds.width = v;
1969
+ this.opts.onPropertyChange();
1970
+ }
1971
+ });
1972
+ new NumberInput(row, {
1973
+ label: "H",
1974
+ value: layer.bounds.height,
1975
+ step: 1,
1976
+ min: 1,
1977
+ onChange: (v) => {
1978
+ layer.bounds.height = v;
1979
+ this.opts.onPropertyChange();
1980
+ }
1981
+ });
1982
+ });
1983
+ this.addSection("Transform", () => {
1984
+ const row1 = this.makeRow(true);
1985
+ new SliderInput(row1, {
1986
+ label: "Opacity",
1987
+ value: layer.opacity ?? 1,
1988
+ min: 0,
1989
+ max: 1,
1990
+ step: 0.01,
1991
+ onChange: (v) => {
1992
+ layer.opacity = v;
1993
+ this.opts.onPropertyChange();
1994
+ }
1995
+ });
1996
+ const row2 = this.makeRow();
1997
+ new NumberInput(row2, {
1998
+ label: "Rotation",
1999
+ value: layer.rotation ?? 0,
2000
+ step: 1,
2001
+ suffix: "\xB0",
2002
+ onChange: (v) => {
2003
+ layer.rotation = v;
2004
+ this.opts.onPropertyChange();
2005
+ }
2006
+ });
2007
+ const row3 = this.makeRow();
2008
+ new NumberInput(row3, {
2009
+ label: "Scale X",
2010
+ value: layer.scale?.x ?? 1,
2011
+ step: 0.1,
2012
+ onChange: (v) => {
2013
+ if (!layer.scale) layer.scale = { x: 1, y: 1 };
2014
+ layer.scale.x = v;
2015
+ this.opts.onPropertyChange();
2016
+ }
2017
+ });
2018
+ new NumberInput(row3, {
2019
+ label: "Scale Y",
2020
+ value: layer.scale?.y ?? 1,
2021
+ step: 0.1,
2022
+ onChange: (v) => {
2023
+ if (!layer.scale) layer.scale = { x: 1, y: 1 };
2024
+ layer.scale.y = v;
2025
+ this.opts.onPropertyChange();
2026
+ }
2027
+ });
2028
+ });
2029
+ this.addShadowSection(layer);
2030
+ this.addVisualSection(layer);
2031
+ const state = this.doc.states[this.stateName];
2032
+ if (state) {
2033
+ this.deltaPanel = new DeltaPanel(this.scroll, {
2034
+ doc: this.doc,
2035
+ state,
2036
+ stateName: this.stateName,
2037
+ layerId: this.layerId,
2038
+ onDeltaChange: () => this.opts.onPropertyChange()
2039
+ });
2040
+ }
2041
+ }
2042
+ addVisualSection(layer) {
2043
+ const visual = layer.visual;
2044
+ if (visual.type === "shape") {
2045
+ this.addSection("Visual", () => {
2046
+ if (visual.fill) {
2047
+ const row = this.makeRow(true);
2048
+ new FillEditor(row, {
2049
+ fill: visual.fill,
2050
+ onChange: (newFill) => {
2051
+ visual.fill = newFill;
2052
+ this.opts.onPropertyChange();
2053
+ }
2054
+ });
2055
+ }
2056
+ if (visual.stroke) {
2057
+ const row = this.makeRow(true);
2058
+ const strokeColor = typeof visual.stroke.color === "string" ? visual.stroke.color : "#000000";
2059
+ new ColorPicker(row, {
2060
+ label: "Stroke",
2061
+ value: strokeColor,
2062
+ onChange: (v) => {
2063
+ if (visual.stroke) {
2064
+ visual.stroke.color = v;
2065
+ this.opts.onPropertyChange();
2066
+ }
2067
+ }
2068
+ });
2069
+ const row2 = this.makeRow(true);
2070
+ new NumberInput(row2, {
2071
+ label: "Stroke Width",
2072
+ value: visual.stroke.width,
2073
+ step: 1,
2074
+ min: 0,
2075
+ onChange: (v) => {
2076
+ if (visual.stroke) {
2077
+ visual.stroke.width = v;
2078
+ this.opts.onPropertyChange();
2079
+ }
2080
+ }
2081
+ });
2082
+ const row2b = this.makeRow();
2083
+ new SliderInput(row2b, {
2084
+ label: "Start",
2085
+ value: visual.stroke.strokeStart ?? 0,
2086
+ min: 0,
2087
+ max: 1,
2088
+ step: 0.01,
2089
+ onChange: (v) => {
2090
+ if (visual.stroke) {
2091
+ visual.stroke.strokeStart = v;
2092
+ this.opts.onPropertyChange();
2093
+ }
2094
+ }
2095
+ });
2096
+ new SliderInput(row2b, {
2097
+ label: "End",
2098
+ value: visual.stroke.strokeEnd ?? 1,
2099
+ min: 0,
2100
+ max: 1,
2101
+ step: 0.01,
2102
+ onChange: (v) => {
2103
+ if (visual.stroke) {
2104
+ visual.stroke.strokeEnd = v;
2105
+ this.opts.onPropertyChange();
2106
+ }
2107
+ }
2108
+ });
2109
+ }
2110
+ if (visual.shape.type === "rect") {
2111
+ const cr = typeof visual.shape.cornerRadius === "number" ? visual.shape.cornerRadius : 0;
2112
+ const row = this.makeRow(true);
2113
+ new NumberInput(row, {
2114
+ label: "Corner Radius",
2115
+ value: cr,
2116
+ step: 1,
2117
+ min: 0,
2118
+ onChange: (v) => {
2119
+ if (visual.shape.type === "rect") {
2120
+ visual.shape.cornerRadius = v;
2121
+ this.opts.onPropertyChange();
2122
+ }
2123
+ }
2124
+ });
2125
+ }
2126
+ });
2127
+ if (visual.shape.type === "path") {
2128
+ this.addSection("Path Points", () => {
2129
+ const points = visual.shape.points;
2130
+ for (let i = 0; i < points.length; i++) {
2131
+ const row = this.makeRow();
2132
+ const label = document.createElement("span");
2133
+ label.className = "atel-control-label";
2134
+ label.textContent = `P${i}`;
2135
+ label.style.minWidth = "24px";
2136
+ row.appendChild(label);
2137
+ const idx = i;
2138
+ new NumberInput(row, {
2139
+ label: "X",
2140
+ value: points[idx].x,
2141
+ step: 1,
2142
+ onChange: (v) => {
2143
+ points[idx].x = v;
2144
+ this.opts.onPropertyChange();
2145
+ }
2146
+ });
2147
+ new NumberInput(row, {
2148
+ label: "Y",
2149
+ value: points[idx].y,
2150
+ step: 1,
2151
+ onChange: (v) => {
2152
+ points[idx].y = v;
2153
+ this.opts.onPropertyChange();
2154
+ }
2155
+ });
2156
+ const rmBtn = document.createElement("button");
2157
+ rmBtn.className = "atel-toolbar__btn";
2158
+ rmBtn.textContent = "\u2715";
2159
+ rmBtn.style.padding = "2px 6px";
2160
+ rmBtn.addEventListener("click", () => {
2161
+ points.splice(idx, 1);
2162
+ this.render();
2163
+ this.opts.onPropertyChange();
2164
+ });
2165
+ row.appendChild(rmBtn);
2166
+ }
2167
+ const closedRow = this.makeRow(true);
2168
+ const closedLabel = document.createElement("label");
2169
+ closedLabel.className = "atel-control-label";
2170
+ closedLabel.textContent = "Closed";
2171
+ const checkbox = document.createElement("input");
2172
+ checkbox.type = "checkbox";
2173
+ checkbox.checked = visual.shape.closed ?? false;
2174
+ checkbox.addEventListener("change", () => {
2175
+ if (visual.shape.type === "path") {
2176
+ visual.shape.closed = checkbox.checked;
2177
+ this.opts.onPropertyChange();
2178
+ }
2179
+ });
2180
+ closedRow.appendChild(closedLabel);
2181
+ closedRow.appendChild(checkbox);
2182
+ const addRow = this.makeRow(true);
2183
+ const addBtn = document.createElement("button");
2184
+ addBtn.className = "atel-toolbar__btn";
2185
+ addBtn.textContent = "+ Add Point";
2186
+ addBtn.addEventListener("click", () => {
2187
+ if (visual.shape.type === "path") {
2188
+ const last = visual.shape.points[visual.shape.points.length - 1] ?? { x: 0, y: 0 };
2189
+ visual.shape.points.push({ x: last.x + 20, y: last.y });
2190
+ this.render();
2191
+ this.opts.onPropertyChange();
2192
+ }
2193
+ });
2194
+ addRow.appendChild(addBtn);
2195
+ });
2196
+ }
2197
+ }
2198
+ if (visual.type === "image") {
2199
+ this.addSection("Image", () => {
2200
+ const iv = visual;
2201
+ const row1 = this.makeRow(true);
2202
+ this.addTextInput(row1, "Asset ID", iv.assetId ?? "", (v) => {
2203
+ iv.assetId = v;
2204
+ this.opts.onPropertyChange();
2205
+ });
2206
+ const row2 = this.makeRow(true);
2207
+ this.addTextInput(row2, "Src", iv.src ?? "", (v) => {
2208
+ iv.src = v;
2209
+ this.opts.onPropertyChange();
2210
+ });
2211
+ if (this.doc && iv.assetId && this.doc.assets?.[iv.assetId]) {
2212
+ const row3 = this.makeRow(true);
2213
+ const useBtn = document.createElement("button");
2214
+ useBtn.className = "atel-toolbar__btn";
2215
+ useBtn.textContent = "Use Asset";
2216
+ useBtn.addEventListener("click", () => {
2217
+ if (this.doc?.assets?.[iv.assetId]) {
2218
+ iv.src = this.doc.assets[iv.assetId].src;
2219
+ this.render();
2220
+ this.opts.onPropertyChange();
2221
+ }
2222
+ });
2223
+ row3.appendChild(useBtn);
2224
+ }
2225
+ });
2226
+ }
2227
+ if (visual.type === "text") {
2228
+ this.addSection("Text", () => {
2229
+ const row1 = this.makeRow(true);
2230
+ this.addTextInput(row1, "Content", visual.content, (v) => {
2231
+ visual.content = v;
2232
+ this.opts.onPropertyChange();
2233
+ });
2234
+ const row2 = this.makeRow();
2235
+ new NumberInput(row2, {
2236
+ label: "Font Size",
2237
+ value: visual.style.fontSize,
2238
+ step: 1,
2239
+ min: 1,
2240
+ onChange: (v) => {
2241
+ visual.style.fontSize = v;
2242
+ this.opts.onPropertyChange();
2243
+ }
2244
+ });
2245
+ const textColor = typeof visual.style.color === "string" ? visual.style.color : "#FFFFFF";
2246
+ new ColorPicker(row2, {
2247
+ label: "Color",
2248
+ value: textColor,
2249
+ onChange: (v) => {
2250
+ visual.style.color = v;
2251
+ this.opts.onPropertyChange();
2252
+ }
2253
+ });
2254
+ const row3 = this.makeRow();
2255
+ new SelectInput(row3, {
2256
+ label: "Align",
2257
+ value: visual.style.textAlign ?? "left",
2258
+ options: [
2259
+ { label: "Left", value: "left" },
2260
+ { label: "Center", value: "center" },
2261
+ { label: "Right", value: "right" }
2262
+ ],
2263
+ onChange: (v) => {
2264
+ visual.style.textAlign = v;
2265
+ this.opts.onPropertyChange();
2266
+ }
2267
+ });
2268
+ });
2269
+ }
2270
+ }
2271
+ addShadowSection(layer) {
2272
+ if (!layer.shadow) {
2273
+ this.addSection("Shadow", () => {
2274
+ const row = this.makeRow(true);
2275
+ const addBtn = document.createElement("button");
2276
+ addBtn.className = "atel-toolbar__btn";
2277
+ addBtn.textContent = "+ Add Shadow";
2278
+ addBtn.addEventListener("click", () => {
2279
+ layer.shadow = { color: "#00000080", blur: 10 };
2280
+ this.render();
2281
+ this.opts.onPropertyChange();
2282
+ });
2283
+ row.appendChild(addBtn);
2284
+ });
2285
+ return;
2286
+ }
2287
+ this.addSection("Shadow", () => {
2288
+ const shadow = layer.shadow;
2289
+ const row1 = this.makeRow(true);
2290
+ const shadowColor = typeof shadow.color === "string" ? shadow.color : "#00000080";
2291
+ new ColorPicker(row1, {
2292
+ label: "Color",
2293
+ value: shadowColor,
2294
+ onChange: (v) => {
2295
+ shadow.color = v;
2296
+ this.opts.onPropertyChange();
2297
+ }
2298
+ });
2299
+ const row2 = this.makeRow(true);
2300
+ new SliderInput(row2, {
2301
+ label: "Blur",
2302
+ value: shadow.blur,
2303
+ min: 0,
2304
+ max: 50,
2305
+ step: 1,
2306
+ onChange: (v) => {
2307
+ shadow.blur = v;
2308
+ this.opts.onPropertyChange();
2309
+ }
2310
+ });
2311
+ const row3 = this.makeRow();
2312
+ new NumberInput(row3, {
2313
+ label: "Offset X",
2314
+ value: shadow.offsetX ?? 0,
2315
+ step: 1,
2316
+ onChange: (v) => {
2317
+ shadow.offsetX = v;
2318
+ this.opts.onPropertyChange();
2319
+ }
2320
+ });
2321
+ new NumberInput(row3, {
2322
+ label: "Offset Y",
2323
+ value: shadow.offsetY ?? 0,
2324
+ step: 1,
2325
+ onChange: (v) => {
2326
+ shadow.offsetY = v;
2327
+ this.opts.onPropertyChange();
2328
+ }
2329
+ });
2330
+ const row4 = this.makeRow(true);
2331
+ const rmBtn = document.createElement("button");
2332
+ rmBtn.className = "atel-toolbar__btn";
2333
+ rmBtn.textContent = "Remove Shadow";
2334
+ rmBtn.addEventListener("click", () => {
2335
+ delete layer.shadow;
2336
+ this.render();
2337
+ this.opts.onPropertyChange();
2338
+ });
2339
+ row4.appendChild(rmBtn);
2340
+ });
2341
+ }
2342
+ addSection(title, buildContent) {
2343
+ const section = document.createElement("div");
2344
+ section.className = "atel-prop-section";
2345
+ const titleEl = document.createElement("div");
2346
+ titleEl.className = "atel-prop-section__title";
2347
+ titleEl.textContent = title;
2348
+ section.appendChild(titleEl);
2349
+ this.scroll.appendChild(section);
2350
+ buildContent();
2351
+ }
2352
+ makeRow(full = false) {
2353
+ const row = document.createElement("div");
2354
+ row.className = full ? "atel-prop-row atel-prop-row--full" : "atel-prop-row";
2355
+ const sections = this.scroll.querySelectorAll(".atel-prop-section");
2356
+ const lastSection = sections[sections.length - 1];
2357
+ if (lastSection) lastSection.appendChild(row);
2358
+ return row;
2359
+ }
2360
+ addTextInput(parent, label, value, onChange) {
2361
+ const wrap = document.createElement("div");
2362
+ const lbl = document.createElement("label");
2363
+ lbl.className = "atel-control-label";
2364
+ lbl.textContent = label;
2365
+ const input = document.createElement("input");
2366
+ input.type = "text";
2367
+ input.className = "atel-input";
2368
+ input.value = value;
2369
+ input.addEventListener("change", () => onChange(input.value));
2370
+ wrap.appendChild(lbl);
2371
+ wrap.appendChild(input);
2372
+ parent.appendChild(wrap);
2373
+ }
2374
+ };
2375
+
2376
+ // src/panels/yaml-panel.ts
2377
+ var import_atelier_schema = require("@a-company/atelier-schema");
2378
+ var YamlPanel = class {
2379
+ el;
2380
+ pre;
2381
+ copyBtn;
2382
+ doc = null;
2383
+ constructor(parent) {
2384
+ this.el = document.createElement("div");
2385
+ this.el.className = "atel-yaml-panel";
2386
+ const header = document.createElement("div");
2387
+ header.className = "atel-panel-header";
2388
+ const label = document.createElement("span");
2389
+ label.textContent = "YAML Source";
2390
+ this.copyBtn = document.createElement("button");
2391
+ this.copyBtn.className = "atel-yaml-copy-btn";
2392
+ this.copyBtn.textContent = "Copy";
2393
+ this.copyBtn.addEventListener("click", () => this.copyToClipboard());
2394
+ header.appendChild(label);
2395
+ header.appendChild(this.copyBtn);
2396
+ this.el.appendChild(header);
2397
+ const content = document.createElement("div");
2398
+ content.className = "atel-yaml-content";
2399
+ this.pre = document.createElement("pre");
2400
+ content.appendChild(this.pre);
2401
+ this.el.appendChild(content);
2402
+ parent.appendChild(this.el);
2403
+ }
2404
+ loadDocument(doc) {
2405
+ this.doc = doc;
2406
+ this.refresh();
2407
+ }
2408
+ refresh() {
2409
+ if (!this.doc) {
2410
+ this.pre.innerHTML = "";
2411
+ return;
2412
+ }
2413
+ const yaml = (0, import_atelier_schema.serializeAtelier)(this.doc);
2414
+ this.pre.innerHTML = highlightYaml(yaml);
2415
+ }
2416
+ async copyToClipboard() {
2417
+ if (!this.doc) return;
2418
+ const yaml = (0, import_atelier_schema.serializeAtelier)(this.doc);
2419
+ try {
2420
+ await navigator.clipboard.writeText(yaml);
2421
+ const orig = this.copyBtn.textContent;
2422
+ this.copyBtn.textContent = "Copied!";
2423
+ setTimeout(() => {
2424
+ this.copyBtn.textContent = orig;
2425
+ }, 1500);
2426
+ } catch {
2427
+ const range = document.createRange();
2428
+ range.selectNodeContents(this.pre);
2429
+ const sel = window.getSelection();
2430
+ sel?.removeAllRanges();
2431
+ sel?.addRange(range);
2432
+ }
2433
+ }
2434
+ };
2435
+ function highlightYaml(yaml) {
2436
+ return yaml.split("\n").map((line) => highlightLine(line)).join("\n");
2437
+ }
2438
+ function highlightLine(line) {
2439
+ if (line.trimStart().startsWith("#")) {
2440
+ return `<span class="atel-yaml-comment">${esc(line)}</span>`;
2441
+ }
2442
+ const kvMatch = line.match(/^(\s*)(- )?([^:]+?)(:)(.*)/);
2443
+ if (kvMatch) {
2444
+ const [, indent, dash, key, colon, rest] = kvMatch;
2445
+ let result = esc(indent);
2446
+ if (dash) result += `<span class="atel-yaml-dash">- </span>`;
2447
+ result += `<span class="atel-yaml-key">${esc(key)}</span>`;
2448
+ result += `<span class="atel-yaml-colon">:</span>`;
2449
+ result += highlightValue(rest);
2450
+ return result;
2451
+ }
2452
+ const listMatch = line.match(/^(\s*)(- )(.*)/);
2453
+ if (listMatch) {
2454
+ const [, indent, dash, val] = listMatch;
2455
+ return esc(indent) + `<span class="atel-yaml-dash">${esc(dash)}</span>` + highlightValue(val);
2456
+ }
2457
+ return esc(line);
2458
+ }
2459
+ function highlightValue(val) {
2460
+ const trimmed = val.trim();
2461
+ if (!trimmed) return esc(val);
2462
+ if (trimmed.startsWith('"') || trimmed.startsWith("'")) {
2463
+ return ` <span class="atel-yaml-string">${esc(trimmed)}</span>`;
2464
+ }
2465
+ if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
2466
+ return ` <span class="atel-yaml-number">${esc(trimmed)}</span>`;
2467
+ }
2468
+ if (trimmed === "true" || trimmed === "false") {
2469
+ return ` <span class="atel-yaml-bool">${esc(trimmed)}</span>`;
2470
+ }
2471
+ if (trimmed === "null" || trimmed === "~") {
2472
+ return ` <span class="atel-yaml-null">${esc(trimmed)}</span>`;
2473
+ }
2474
+ return ` <span class="atel-yaml-string">${esc(trimmed)}</span>`;
2475
+ }
2476
+ function esc(s) {
2477
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2478
+ }
2479
+
2480
+ // src/theme.ts
2481
+ var defaultTheme = {
2482
+ bg: "#2C2C2C",
2483
+ bgSecondary: "#333333",
2484
+ bgTertiary: "#3D3D3D",
2485
+ text: "#F5F0EB",
2486
+ textMuted: "#A89F95",
2487
+ textAccent: "#F5F0EB",
2488
+ border: "#4A4A4A",
2489
+ buttonBg: "#3D3D3D",
2490
+ buttonHover: "#4A4A4A",
2491
+ buttonActive: "#555555",
2492
+ accent: "#C75B39",
2493
+ accentHover: "#D4724E",
2494
+ sliderTrack: "#4A4A4A",
2495
+ sliderThumb: "#C75B39",
2496
+ fontFamily: "'Cormorant Garamond', Georgia, serif",
2497
+ fontMono: "'SF Mono', 'Fira Code', monospace",
2498
+ canvasShadow: "0 4px 60px rgba(199, 91, 57, 0.12), 0 0 40px rgba(0,0,0,0.4)"
2499
+ };
2500
+ function applyStudioTheme(container, theme) {
2501
+ const s = container.style;
2502
+ s.setProperty("--atel-bg", theme.bg);
2503
+ s.setProperty("--atel-bg-secondary", theme.bgSecondary);
2504
+ s.setProperty("--atel-bg-tertiary", theme.bgTertiary);
2505
+ s.setProperty("--atel-text", theme.text);
2506
+ s.setProperty("--atel-text-muted", theme.textMuted);
2507
+ s.setProperty("--atel-text-accent", theme.textAccent);
2508
+ s.setProperty("--atel-border", theme.border);
2509
+ s.setProperty("--atel-button-bg", theme.buttonBg);
2510
+ s.setProperty("--atel-button-hover", theme.buttonHover);
2511
+ s.setProperty("--atel-button-active", theme.buttonActive);
2512
+ s.setProperty("--atel-accent", theme.accent);
2513
+ s.setProperty("--atel-accent-hover", theme.accentHover);
2514
+ s.setProperty("--atel-slider-track", theme.sliderTrack);
2515
+ s.setProperty("--atel-slider-thumb", theme.sliderThumb);
2516
+ s.setProperty("--atel-font-family", theme.fontFamily);
2517
+ s.setProperty("--atel-font-mono", theme.fontMono);
2518
+ s.setProperty("--atel-canvas-shadow", theme.canvasShadow);
2519
+ }
2520
+
2521
+ // src/history.ts
2522
+ var History = class {
2523
+ undoStack = [];
2524
+ redoStack = [];
2525
+ maxSize = 50;
2526
+ /** Push a snapshot (JSON string of the doc before mutation). Clears redo stack. */
2527
+ snapshot(json) {
2528
+ this.undoStack.push(json);
2529
+ if (this.undoStack.length > this.maxSize) {
2530
+ this.undoStack.shift();
2531
+ }
2532
+ this.redoStack.length = 0;
2533
+ }
2534
+ /** Undo: pops from undo stack, pushes current state to redo, returns the restored JSON. */
2535
+ undo(currentJson) {
2536
+ const prev = this.undoStack.pop();
2537
+ if (prev === void 0) return null;
2538
+ this.redoStack.push(currentJson);
2539
+ return prev;
2540
+ }
2541
+ /** Redo: pops from redo stack, pushes current state to undo, returns the restored JSON. */
2542
+ redo(currentJson) {
2543
+ const next = this.redoStack.pop();
2544
+ if (next === void 0) return null;
2545
+ this.undoStack.push(currentJson);
2546
+ return next;
2547
+ }
2548
+ get canUndo() {
2549
+ return this.undoStack.length > 0;
2550
+ }
2551
+ get canRedo() {
2552
+ return this.redoStack.length > 0;
2553
+ }
2554
+ clear() {
2555
+ this.undoStack.length = 0;
2556
+ this.redoStack.length = 0;
2557
+ }
2558
+ };
2559
+
2560
+ // src/export/video-exporter.ts
2561
+ var import_atelier_core2 = require("@a-company/atelier-core");
2562
+ var import_atelier_canvas2 = require("@a-company/atelier-canvas");
2563
+
2564
+ // src/export/gif-encoder.ts
2565
+ function createGifEncoder(width, height) {
2566
+ const chunks = [];
2567
+ const header = new Uint8Array([
2568
+ 71,
2569
+ 73,
2570
+ 70,
2571
+ 56,
2572
+ 57,
2573
+ 97,
2574
+ // "GIF89a"
2575
+ width & 255,
2576
+ width >> 8 & 255,
2577
+ height & 255,
2578
+ height >> 8 & 255,
2579
+ 247,
2580
+ // GCT flag, 8-bit color, 256 entries (7+1=8 → 2^8=256)
2581
+ 0,
2582
+ // background color index
2583
+ 0
2584
+ // pixel aspect ratio
2585
+ ]);
2586
+ chunks.push(header);
2587
+ let globalPalette = null;
2588
+ const loopExt = new Uint8Array([
2589
+ 33,
2590
+ 255,
2591
+ 11,
2592
+ 78,
2593
+ 69,
2594
+ 84,
2595
+ 83,
2596
+ 67,
2597
+ 65,
2598
+ 80,
2599
+ 69,
2600
+ // "NETSCAPE"
2601
+ 50,
2602
+ 46,
2603
+ 48,
2604
+ // "2.0"
2605
+ 3,
2606
+ 1,
2607
+ 0,
2608
+ 0,
2609
+ // loop count = 0 (infinite)
2610
+ 0
2611
+ // terminator
2612
+ ]);
2613
+ let frameCount = 0;
2614
+ function addFrame(pixels, delay) {
2615
+ const { palette, indices } = quantize(pixels, width, height);
2616
+ if (frameCount === 0) {
2617
+ globalPalette = palette;
2618
+ chunks.push(globalPalette);
2619
+ chunks.push(loopExt);
2620
+ }
2621
+ const gce = new Uint8Array([
2622
+ 33,
2623
+ 249,
2624
+ 4,
2625
+ 8,
2626
+ // disposal = restore to background, no transparency
2627
+ delay & 255,
2628
+ delay >> 8 & 255,
2629
+ 0,
2630
+ 0
2631
+ ]);
2632
+ chunks.push(gce);
2633
+ const imgDesc = new Uint8Array([
2634
+ 44,
2635
+ 0,
2636
+ 0,
2637
+ 0,
2638
+ 0,
2639
+ width & 255,
2640
+ width >> 8 & 255,
2641
+ height & 255,
2642
+ height >> 8 & 255,
2643
+ frameCount === 0 ? 0 : 135
2644
+ ]);
2645
+ chunks.push(imgDesc);
2646
+ if (frameCount > 0) {
2647
+ chunks.push(palette);
2648
+ }
2649
+ chunks.push(lzwEncode(indices, 8));
2650
+ frameCount++;
2651
+ }
2652
+ function finish() {
2653
+ if (frameCount === 0 && !globalPalette) {
2654
+ globalPalette = new Uint8Array(256 * 3);
2655
+ chunks.push(globalPalette);
2656
+ chunks.push(loopExt);
2657
+ }
2658
+ chunks.push(new Uint8Array([59]));
2659
+ let total = 0;
2660
+ for (const c of chunks) total += c.length;
2661
+ const out = new Uint8Array(total);
2662
+ let offset = 0;
2663
+ for (const c of chunks) {
2664
+ out.set(c, offset);
2665
+ offset += c.length;
2666
+ }
2667
+ return out;
2668
+ }
2669
+ return { addFrame, finish };
2670
+ }
2671
+ var freqTable = new Uint32Array(32768);
2672
+ function quantize(pixels, width, height) {
2673
+ const pixelCount = width * height;
2674
+ freqTable.fill(0);
2675
+ const pixelKeys = new Uint16Array(pixelCount);
2676
+ for (let i = 0; i < pixelCount; i++) {
2677
+ const off = i * 4;
2678
+ const key = pixels[off] >> 3 << 10 | pixels[off + 1] >> 3 << 5 | pixels[off + 2] >> 3;
2679
+ freqTable[key]++;
2680
+ pixelKeys[i] = key;
2681
+ }
2682
+ const entries = [];
2683
+ for (let k = 0; k < 32768; k++) {
2684
+ if (freqTable[k] > 0) entries.push({ key: k, count: freqTable[k] });
2685
+ }
2686
+ let top;
2687
+ if (entries.length <= 256) {
2688
+ top = entries;
2689
+ } else {
2690
+ entries.sort((a, b) => b.count - a.count);
2691
+ top = entries.slice(0, 256);
2692
+ }
2693
+ const palette = new Uint8Array(256 * 3);
2694
+ const paletteR5 = new Uint8Array(256);
2695
+ const paletteG5 = new Uint8Array(256);
2696
+ const paletteB5 = new Uint8Array(256);
2697
+ const paletteCount = top.length;
2698
+ const directLut = new Int16Array(32768);
2699
+ directLut.fill(-1);
2700
+ for (let i = 0; i < paletteCount; i++) {
2701
+ const key = top[i].key;
2702
+ const r5 = key >> 10 & 31;
2703
+ const g5 = key >> 5 & 31;
2704
+ const b5 = key & 31;
2705
+ palette[i * 3] = r5 << 3;
2706
+ palette[i * 3 + 1] = g5 << 3;
2707
+ palette[i * 3 + 2] = b5 << 3;
2708
+ paletteR5[i] = r5;
2709
+ paletteG5[i] = g5;
2710
+ paletteB5[i] = b5;
2711
+ directLut[key] = i;
2712
+ }
2713
+ const colorLut = new Uint8Array(32768);
2714
+ for (let k = 0; k < 32768; k++) {
2715
+ const direct = directLut[k];
2716
+ if (direct >= 0) {
2717
+ colorLut[k] = direct;
2718
+ continue;
2719
+ }
2720
+ const r = k >> 10 & 31;
2721
+ const g = k >> 5 & 31;
2722
+ const b = k & 31;
2723
+ let best = 0;
2724
+ let bestDist = 2147483647;
2725
+ for (let i = 0; i < paletteCount; i++) {
2726
+ const dr = r - paletteR5[i];
2727
+ const dg = g - paletteG5[i];
2728
+ const db = b - paletteB5[i];
2729
+ const dist = dr * dr + dg * dg + db * db;
2730
+ if (dist < bestDist) {
2731
+ bestDist = dist;
2732
+ best = i;
2733
+ if (dist === 0) break;
2734
+ }
2735
+ }
2736
+ colorLut[k] = best;
2737
+ }
2738
+ const indices = new Uint8Array(pixelCount);
2739
+ for (let i = 0; i < pixelCount; i++) {
2740
+ indices[i] = colorLut[pixelKeys[i]];
2741
+ }
2742
+ return { palette, indices };
2743
+ }
2744
+ var LZW_HASH_SIZE = 8192;
2745
+ var LZW_HASH_MASK = LZW_HASH_SIZE - 1;
2746
+ var hashKeys = new Int32Array(LZW_HASH_SIZE);
2747
+ var hashVals = new Int16Array(LZW_HASH_SIZE);
2748
+ function lzwEncode(indices, minCodeSize) {
2749
+ const clearCode = 1 << minCodeSize;
2750
+ const eoiCode = clearCode + 1;
2751
+ const maxDictSize = 4096;
2752
+ let buf = new Uint8Array(256 + (indices.length >>> 1));
2753
+ let bufPos = 0;
2754
+ buf[bufPos++] = minCodeSize;
2755
+ let subBlockStart = bufPos;
2756
+ buf[bufPos++] = 0;
2757
+ let subBlockLen = 0;
2758
+ let bitBuf = 0;
2759
+ let bitCount = 0;
2760
+ function ensureCapacity(need) {
2761
+ if (bufPos + need >= buf.length) {
2762
+ const next = new Uint8Array(Math.max(buf.length * 2, bufPos + need + 256));
2763
+ next.set(buf);
2764
+ buf = next;
2765
+ }
2766
+ }
2767
+ function writeBits(code, codeSize2) {
2768
+ bitBuf |= code << bitCount;
2769
+ bitCount += codeSize2;
2770
+ while (bitCount >= 8) {
2771
+ ensureCapacity(4);
2772
+ buf[bufPos++] = bitBuf & 255;
2773
+ bitBuf >>>= 8;
2774
+ bitCount -= 8;
2775
+ subBlockLen++;
2776
+ if (subBlockLen === 255) {
2777
+ buf[subBlockStart] = 255;
2778
+ subBlockStart = bufPos;
2779
+ buf[bufPos++] = 0;
2780
+ subBlockLen = 0;
2781
+ }
2782
+ }
2783
+ }
2784
+ function flushBits() {
2785
+ if (bitCount > 0) {
2786
+ ensureCapacity(4);
2787
+ buf[bufPos++] = bitBuf & 255;
2788
+ bitBuf = 0;
2789
+ bitCount = 0;
2790
+ subBlockLen++;
2791
+ }
2792
+ if (subBlockLen > 0) {
2793
+ buf[subBlockStart] = subBlockLen;
2794
+ ensureCapacity(1);
2795
+ buf[bufPos++] = 0;
2796
+ } else {
2797
+ bufPos = subBlockStart;
2798
+ ensureCapacity(1);
2799
+ buf[bufPos++] = 0;
2800
+ }
2801
+ }
2802
+ let codeSize = minCodeSize + 1;
2803
+ let nextCode = eoiCode + 1;
2804
+ function resetDict() {
2805
+ hashKeys.fill(-1);
2806
+ codeSize = minCodeSize + 1;
2807
+ nextCode = eoiCode + 1;
2808
+ }
2809
+ function hashLookup(prefix, suffix) {
2810
+ const key = prefix << 8 | suffix;
2811
+ let slot = key * 2654435761 & LZW_HASH_MASK;
2812
+ while (true) {
2813
+ if (hashKeys[slot] === key) return hashVals[slot];
2814
+ if (hashKeys[slot] === -1) return -1;
2815
+ slot = slot + 1 & LZW_HASH_MASK;
2816
+ }
2817
+ }
2818
+ function hashInsert(prefix, suffix, code) {
2819
+ const key = prefix << 8 | suffix;
2820
+ let slot = key * 2654435761 & LZW_HASH_MASK;
2821
+ while (hashKeys[slot] !== -1) {
2822
+ slot = slot + 1 & LZW_HASH_MASK;
2823
+ }
2824
+ hashKeys[slot] = key;
2825
+ hashVals[slot] = code;
2826
+ }
2827
+ resetDict();
2828
+ writeBits(clearCode, codeSize);
2829
+ if (indices.length === 0) {
2830
+ writeBits(eoiCode, codeSize);
2831
+ flushBits();
2832
+ return buf.subarray(0, bufPos);
2833
+ }
2834
+ let current = indices[0];
2835
+ for (let i = 1; i < indices.length; i++) {
2836
+ const suffix = indices[i];
2837
+ const found = hashLookup(current, suffix);
2838
+ if (found >= 0) {
2839
+ current = found;
2840
+ } else {
2841
+ writeBits(current, codeSize);
2842
+ if (nextCode < maxDictSize) {
2843
+ hashInsert(current, suffix, nextCode++);
2844
+ if (nextCode > 1 << codeSize && codeSize < 12) {
2845
+ codeSize++;
2846
+ }
2847
+ } else {
2848
+ writeBits(clearCode, codeSize);
2849
+ resetDict();
2850
+ }
2851
+ current = suffix;
2852
+ }
2853
+ }
2854
+ writeBits(current, codeSize);
2855
+ writeBits(eoiCode, codeSize);
2856
+ flushBits();
2857
+ return buf.subarray(0, bufPos);
2858
+ }
2859
+
2860
+ // src/export/video-exporter.ts
2861
+ function supportsWebCodecs() {
2862
+ return typeof VideoEncoder !== "undefined" && typeof VideoFrame !== "undefined";
2863
+ }
2864
+ function supportsWebM() {
2865
+ return supportsWebCodecs();
2866
+ }
2867
+ function supportsGifExport() {
2868
+ return true;
2869
+ }
2870
+ async function exportDocument(doc, canvas, imageCache, opts) {
2871
+ if (opts.format === "mp4") {
2872
+ return exportMP4(doc, canvas, imageCache, opts);
2873
+ }
2874
+ if (opts.format === "webm") {
2875
+ return exportWebM(doc, canvas, imageCache, opts);
2876
+ }
2877
+ return exportGIF(doc, canvas, imageCache, opts);
2878
+ }
2879
+ function createManagedEncoder(onChunk) {
2880
+ let encError = null;
2881
+ const encoder = new VideoEncoder({
2882
+ output: (chunk, meta) => {
2883
+ onChunk(chunk, meta ?? void 0);
2884
+ },
2885
+ error: (e) => {
2886
+ encError = e;
2887
+ }
2888
+ });
2889
+ function checkError() {
2890
+ if (encError) {
2891
+ const msg = encError.message;
2892
+ encError = null;
2893
+ throw new Error(`VideoEncoder error: ${msg}`);
2894
+ }
2895
+ if (encoder.state === "closed") {
2896
+ throw new Error("VideoEncoder closed unexpectedly.");
2897
+ }
2898
+ }
2899
+ async function waitForDrain() {
2900
+ while (encoder.state === "configured" && encoder.encodeQueueSize > 5) {
2901
+ await yieldToMain();
2902
+ if (encError) checkError();
2903
+ }
2904
+ }
2905
+ return { encoder, checkError, waitForDrain };
2906
+ }
2907
+ async function exportMP4(doc, canvas, imageCache, opts) {
2908
+ if (!supportsWebCodecs()) {
2909
+ throw new Error("WebCodecs API not available \u2014 MP4 export requires a modern browser.");
2910
+ }
2911
+ const { Muxer, ArrayBufferTarget } = await import("mp4-muxer");
2912
+ const fps = doc.canvas.fps ?? 30;
2913
+ const width = doc.canvas.width;
2914
+ const height = doc.canvas.height;
2915
+ const encWidth = width % 2 === 0 ? width : width + 1;
2916
+ const encHeight = height % 2 === 0 ? height : height + 1;
2917
+ const codecConfig = {
2918
+ codec: "avc1.42001f",
2919
+ width: encWidth,
2920
+ height: encHeight,
2921
+ bitrate: 5e6,
2922
+ framerate: fps
2923
+ };
2924
+ const support = await VideoEncoder.isConfigSupported(codecConfig);
2925
+ if (!support.supported) {
2926
+ throw new Error(
2927
+ `H.264 codec not supported for ${encWidth}\xD7${encHeight} at ${fps}fps. Try exporting as GIF or WebM instead.`
2928
+ );
2929
+ }
2930
+ const target = new ArrayBufferTarget();
2931
+ const muxer = new Muxer({
2932
+ target,
2933
+ video: {
2934
+ codec: "avc",
2935
+ width: encWidth,
2936
+ height: encHeight
2937
+ },
2938
+ fastStart: "in-memory"
2939
+ });
2940
+ const { encoder, checkError, waitForDrain } = createManagedEncoder(
2941
+ (chunk, meta) => muxer.addVideoChunk(chunk, meta)
2942
+ );
2943
+ encoder.configure(support.config);
2944
+ await yieldToMain();
2945
+ checkError();
2946
+ const frames = collectFrames(doc, opts.states);
2947
+ const totalFrames = frames.length;
2948
+ const frameDurationUs = Math.round(1e6 / fps);
2949
+ const ctx = canvas.getContext("2d");
2950
+ const origWidth = canvas.width;
2951
+ const origHeight = canvas.height;
2952
+ if (encWidth !== width || encHeight !== height) {
2953
+ canvas.width = encWidth;
2954
+ canvas.height = encHeight;
2955
+ }
2956
+ try {
2957
+ for (let i = 0; i < totalFrames; i++) {
2958
+ checkError();
2959
+ await waitForDrain();
2960
+ const { state, frame } = frames[i];
2961
+ const resolved = (0, import_atelier_core2.resolveFrame)(doc, state, frame);
2962
+ (0, import_atelier_canvas2.renderFrame)(ctx, resolved, doc, imageCache);
2963
+ const timestamp = i * frameDurationUs;
2964
+ const videoFrame = new VideoFrame(canvas, { timestamp });
2965
+ encoder.encode(videoFrame, { keyFrame: i % 60 === 0 });
2966
+ videoFrame.close();
2967
+ opts.onProgress?.({
2968
+ frame: i + 1,
2969
+ total: totalFrames,
2970
+ percent: Math.round((i + 1) / totalFrames * 100)
2971
+ });
2972
+ if (i % 10 === 9) await yieldToMain();
2973
+ }
2974
+ await encoder.flush();
2975
+ checkError();
2976
+ encoder.close();
2977
+ muxer.finalize();
2978
+ } finally {
2979
+ if (encWidth !== width || encHeight !== height) {
2980
+ canvas.width = origWidth;
2981
+ canvas.height = origHeight;
2982
+ }
2983
+ }
2984
+ const durationMs = Math.round(totalFrames / fps * 1e3);
2985
+ return {
2986
+ blob: new Blob([target.buffer], { type: "video/mp4" }),
2987
+ format: "mp4",
2988
+ totalFrames,
2989
+ durationMs
2990
+ };
2991
+ }
2992
+ async function exportWebM(doc, _canvas, imageCache, opts) {
2993
+ if (!supportsWebCodecs()) {
2994
+ throw new Error("WebCodecs API not available \u2014 WebM export requires a modern browser.");
2995
+ }
2996
+ const { Muxer, ArrayBufferTarget } = await import("webm-muxer");
2997
+ const fps = doc.canvas.fps ?? 30;
2998
+ const width = doc.canvas.width;
2999
+ const height = doc.canvas.height;
3000
+ const codecCandidates = [
3001
+ {
3002
+ codec: "vp09.00.10.08",
3003
+ width,
3004
+ height,
3005
+ bitrate: 5e6,
3006
+ framerate: fps,
3007
+ alpha: "keep"
3008
+ },
3009
+ {
3010
+ codec: "vp09.00.10.08",
3011
+ width,
3012
+ height,
3013
+ bitrate: 5e6,
3014
+ framerate: fps
3015
+ },
3016
+ {
3017
+ codec: "vp8",
3018
+ width,
3019
+ height,
3020
+ bitrate: 5e6,
3021
+ framerate: fps
3022
+ }
3023
+ ];
3024
+ let chosenConfig = null;
3025
+ for (const candidate of codecCandidates) {
3026
+ const support = await VideoEncoder.isConfigSupported(candidate);
3027
+ if (support.supported) {
3028
+ chosenConfig = support.config;
3029
+ break;
3030
+ }
3031
+ }
3032
+ if (!chosenConfig) {
3033
+ throw new Error(
3034
+ `No supported VP9/VP8 codec for ${width}\xD7${height}. Try exporting as MP4 or GIF instead.`
3035
+ );
3036
+ }
3037
+ const hasAlpha = chosenConfig.alpha === "keep";
3038
+ const muxCodec = chosenConfig.codec.startsWith("vp09") ? "V_VP9" : "V_VP8";
3039
+ const target = new ArrayBufferTarget();
3040
+ const muxer = new Muxer({
3041
+ target,
3042
+ video: {
3043
+ codec: muxCodec,
3044
+ width,
3045
+ height
3046
+ }
3047
+ });
3048
+ const { encoder, checkError, waitForDrain } = createManagedEncoder(
3049
+ (chunk, meta) => muxer.addVideoChunk(chunk, meta)
3050
+ );
3051
+ encoder.configure(chosenConfig);
3052
+ await yieldToMain();
3053
+ checkError();
3054
+ const frames = collectFrames(doc, opts.states);
3055
+ const totalFrames = frames.length;
3056
+ const frameDurationUs = Math.round(1e6 / fps);
3057
+ const alphaCanvas = document.createElement("canvas");
3058
+ alphaCanvas.width = width;
3059
+ alphaCanvas.height = height;
3060
+ const ctx = alphaCanvas.getContext("2d", { alpha: hasAlpha });
3061
+ for (let i = 0; i < totalFrames; i++) {
3062
+ checkError();
3063
+ await waitForDrain();
3064
+ const { state, frame } = frames[i];
3065
+ const resolved = (0, import_atelier_core2.resolveFrame)(doc, state, frame);
3066
+ ctx.clearRect(0, 0, width, height);
3067
+ (0, import_atelier_canvas2.renderFrame)(ctx, resolved, doc, imageCache);
3068
+ const timestamp = i * frameDurationUs;
3069
+ const frameOpts = { timestamp };
3070
+ if (hasAlpha) frameOpts.alpha = "keep";
3071
+ const videoFrame = new VideoFrame(alphaCanvas, frameOpts);
3072
+ encoder.encode(videoFrame, { keyFrame: i % 60 === 0 });
3073
+ videoFrame.close();
3074
+ opts.onProgress?.({
3075
+ frame: i + 1,
3076
+ total: totalFrames,
3077
+ percent: Math.round((i + 1) / totalFrames * 100)
3078
+ });
3079
+ if (i % 10 === 9) await yieldToMain();
3080
+ }
3081
+ await encoder.flush();
3082
+ checkError();
3083
+ encoder.close();
3084
+ muxer.finalize();
3085
+ const durationMs = Math.round(totalFrames / fps * 1e3);
3086
+ return {
3087
+ blob: new Blob([target.buffer], { type: "video/webm" }),
3088
+ format: "webm",
3089
+ totalFrames,
3090
+ durationMs
3091
+ };
3092
+ }
3093
+ async function exportGIF(doc, canvas, imageCache, opts) {
3094
+ const fps = doc.canvas.fps ?? 30;
3095
+ const width = doc.canvas.width;
3096
+ const height = doc.canvas.height;
3097
+ const ctx = canvas.getContext("2d");
3098
+ const frames = collectFrames(doc, opts.states);
3099
+ const totalFrames = frames.length;
3100
+ const encoder = createGifEncoder(width, height);
3101
+ const MIN_DELAY_CS = 3;
3102
+ const minFrameMs = MIN_DELAY_CS * 10;
3103
+ let accGifTimeMs = 0;
3104
+ let gifFrameCount = 0;
3105
+ for (let i = 0; i < totalFrames; i++) {
3106
+ const frameEndMs = (i + 1) * 1e3 / fps;
3107
+ if (frameEndMs - accGifTimeMs < minFrameMs && i < totalFrames - 1) {
3108
+ opts.onProgress?.({
3109
+ frame: i + 1,
3110
+ total: totalFrames,
3111
+ percent: Math.round((i + 1) / totalFrames * 100)
3112
+ });
3113
+ continue;
3114
+ }
3115
+ const { state, frame } = frames[i];
3116
+ const resolved = (0, import_atelier_core2.resolveFrame)(doc, state, frame);
3117
+ (0, import_atelier_canvas2.renderFrame)(ctx, resolved, doc, imageCache);
3118
+ const delayCentiseconds = Math.max(MIN_DELAY_CS, Math.round((frameEndMs - accGifTimeMs) / 10));
3119
+ accGifTimeMs += delayCentiseconds * 10;
3120
+ const imageData = ctx.getImageData(0, 0, width, height);
3121
+ encoder.addFrame(imageData.data, delayCentiseconds);
3122
+ gifFrameCount++;
3123
+ opts.onProgress?.({
3124
+ frame: i + 1,
3125
+ total: totalFrames,
3126
+ percent: Math.round((i + 1) / totalFrames * 100)
3127
+ });
3128
+ if (gifFrameCount % 10 === 9) await yieldToMain();
3129
+ }
3130
+ const bytes = encoder.finish();
3131
+ const durationMs = Math.round(totalFrames / fps * 1e3);
3132
+ return {
3133
+ blob: new Blob([bytes.buffer], { type: "image/gif" }),
3134
+ format: "gif",
3135
+ totalFrames,
3136
+ durationMs
3137
+ };
3138
+ }
3139
+ function collectFrames(doc, stateNames) {
3140
+ const names = stateNames ?? Object.keys(doc.states);
3141
+ const entries = [];
3142
+ for (const state of names) {
3143
+ const s = doc.states[state];
3144
+ if (!s) continue;
3145
+ for (let f = 0; f < s.duration; f++) {
3146
+ entries.push({ state, frame: f });
3147
+ }
3148
+ }
3149
+ return entries;
3150
+ }
3151
+ function yieldToMain() {
3152
+ return new Promise((resolve) => setTimeout(resolve, 0));
3153
+ }
3154
+
3155
+ // src/export/export-dialog.ts
3156
+ var ExportDialog = class {
3157
+ overlay = null;
3158
+ dialog = null;
3159
+ blobUrl = null;
3160
+ parentEl = null;
3161
+ /**
3162
+ * Mount the export overlay into the given parent element.
3163
+ * Typically called with the studio root.
3164
+ */
3165
+ show(doc, canvas, imageCache, parentEl) {
3166
+ this.parentEl = parentEl;
3167
+ this.overlay = document.createElement("div");
3168
+ this.overlay.className = "atel-export-overlay";
3169
+ this.overlay.addEventListener("click", (e) => {
3170
+ if (e.target === this.overlay) this.destroy();
3171
+ });
3172
+ this.dialog = document.createElement("div");
3173
+ this.dialog.className = "atel-export-dialog";
3174
+ this.overlay.appendChild(this.dialog);
3175
+ parentEl.appendChild(this.overlay);
3176
+ this.renderOptions(doc, canvas, imageCache);
3177
+ }
3178
+ destroy() {
3179
+ if (this.blobUrl) {
3180
+ URL.revokeObjectURL(this.blobUrl);
3181
+ this.blobUrl = null;
3182
+ }
3183
+ if (this.overlay && this.parentEl) {
3184
+ this.parentEl.removeChild(this.overlay);
3185
+ }
3186
+ this.overlay = null;
3187
+ this.dialog = null;
3188
+ this.parentEl = null;
3189
+ }
3190
+ // ── Options screen ─────────────────────────────────────────
3191
+ renderOptions(doc, canvas, imageCache) {
3192
+ if (!this.dialog) return;
3193
+ this.dialog.innerHTML = "";
3194
+ const title = document.createElement("div");
3195
+ title.className = "atel-export-dialog__title";
3196
+ title.textContent = "Export Animation";
3197
+ this.dialog.appendChild(title);
3198
+ const formatRow = document.createElement("div");
3199
+ formatRow.className = "atel-export-dialog__row";
3200
+ const formatLabel = document.createElement("label");
3201
+ formatLabel.className = "atel-control-label";
3202
+ formatLabel.textContent = "FORMAT";
3203
+ formatRow.appendChild(formatLabel);
3204
+ const formatSelect = document.createElement("select");
3205
+ formatSelect.className = "atel-toolbar__select atel-export-dialog__select";
3206
+ const gifOpt = document.createElement("option");
3207
+ gifOpt.value = "gif";
3208
+ gifOpt.textContent = "GIF";
3209
+ formatSelect.appendChild(gifOpt);
3210
+ const mp4Opt = document.createElement("option");
3211
+ mp4Opt.value = "mp4";
3212
+ if (supportsWebCodecs()) {
3213
+ mp4Opt.textContent = "MP4";
3214
+ } else {
3215
+ mp4Opt.textContent = "MP4 (not supported)";
3216
+ mp4Opt.disabled = true;
3217
+ }
3218
+ formatSelect.appendChild(mp4Opt);
3219
+ const webmOpt = document.createElement("option");
3220
+ webmOpt.value = "webm";
3221
+ if (supportsWebCodecs()) {
3222
+ webmOpt.textContent = "WebM (transparent)";
3223
+ } else {
3224
+ webmOpt.textContent = "WebM (not supported)";
3225
+ webmOpt.disabled = true;
3226
+ }
3227
+ formatSelect.appendChild(webmOpt);
3228
+ formatRow.appendChild(formatSelect);
3229
+ this.dialog.appendChild(formatRow);
3230
+ const fps = doc.canvas.fps ?? 30;
3231
+ const stateNames = Object.keys(doc.states);
3232
+ let totalFrames = 0;
3233
+ for (const name of stateNames) {
3234
+ totalFrames += doc.states[name]?.duration ?? 0;
3235
+ }
3236
+ const info = document.createElement("div");
3237
+ info.className = "atel-export-dialog__info";
3238
+ info.textContent = `${doc.canvas.width}\xD7${doc.canvas.height} \u2022 ${fps} fps \u2022 ${totalFrames} frames \u2022 ${stateNames.length} state${stateNames.length !== 1 ? "s" : ""}`;
3239
+ this.dialog.appendChild(info);
3240
+ const btnRow = document.createElement("div");
3241
+ btnRow.className = "atel-export-dialog__actions";
3242
+ const cancelBtn = document.createElement("button");
3243
+ cancelBtn.className = "atel-toolbar__btn";
3244
+ cancelBtn.textContent = "Cancel";
3245
+ cancelBtn.addEventListener("click", () => this.destroy());
3246
+ btnRow.appendChild(cancelBtn);
3247
+ const exportBtn = document.createElement("button");
3248
+ exportBtn.className = "atel-toolbar__btn atel-toolbar__btn--active";
3249
+ exportBtn.textContent = "Export";
3250
+ exportBtn.addEventListener("click", () => {
3251
+ const format = formatSelect.value;
3252
+ this.renderProgress(doc, canvas, imageCache, format);
3253
+ });
3254
+ btnRow.appendChild(exportBtn);
3255
+ this.dialog.appendChild(btnRow);
3256
+ }
3257
+ // ── Progress screen ────────────────────────────────────────
3258
+ renderProgress(doc, canvas, imageCache, format) {
3259
+ if (!this.dialog) return;
3260
+ this.dialog.innerHTML = "";
3261
+ const title = document.createElement("div");
3262
+ title.className = "atel-export-dialog__title";
3263
+ title.textContent = "Exporting\u2026";
3264
+ this.dialog.appendChild(title);
3265
+ const progress = document.createElement("progress");
3266
+ progress.className = "atel-export-dialog__progress";
3267
+ progress.max = 100;
3268
+ progress.value = 0;
3269
+ this.dialog.appendChild(progress);
3270
+ const statusText = document.createElement("div");
3271
+ statusText.className = "atel-export-dialog__status";
3272
+ statusText.textContent = "Preparing\u2026";
3273
+ this.dialog.appendChild(statusText);
3274
+ exportDocument(doc, canvas, imageCache, {
3275
+ format,
3276
+ onProgress: ({ frame, total, percent }) => {
3277
+ progress.value = percent;
3278
+ statusText.textContent = `Rendering frame ${frame} / ${total}`;
3279
+ }
3280
+ }).then((result) => {
3281
+ this.renderDone(result);
3282
+ }).catch((err) => {
3283
+ this.renderError(err);
3284
+ });
3285
+ }
3286
+ // ── Done screen ────────────────────────────────────────────
3287
+ renderDone(result) {
3288
+ if (!this.dialog) return;
3289
+ this.dialog.innerHTML = "";
3290
+ const title = document.createElement("div");
3291
+ title.className = "atel-export-dialog__title";
3292
+ title.textContent = "Export Complete";
3293
+ this.dialog.appendChild(title);
3294
+ const info = document.createElement("div");
3295
+ info.className = "atel-export-dialog__info";
3296
+ const sizeMB = (result.blob.size / (1024 * 1024)).toFixed(2);
3297
+ const durationSec = (result.durationMs / 1e3).toFixed(1);
3298
+ info.textContent = `${result.totalFrames} frames \u2022 ${durationSec}s \u2022 ${sizeMB} MB`;
3299
+ this.dialog.appendChild(info);
3300
+ this.blobUrl = URL.createObjectURL(result.blob);
3301
+ const ext = result.format;
3302
+ const downloadBtn = document.createElement("a");
3303
+ downloadBtn.href = this.blobUrl;
3304
+ downloadBtn.download = `animation.${ext}`;
3305
+ downloadBtn.className = "atel-toolbar__btn atel-toolbar__btn--active atel-export-download";
3306
+ downloadBtn.textContent = `Download .${ext}`;
3307
+ this.dialog.appendChild(downloadBtn);
3308
+ const closeBtn = document.createElement("button");
3309
+ closeBtn.className = "atel-toolbar__btn";
3310
+ closeBtn.textContent = "Close";
3311
+ closeBtn.style.marginTop = "8px";
3312
+ closeBtn.addEventListener("click", () => this.destroy());
3313
+ this.dialog.appendChild(closeBtn);
3314
+ }
3315
+ // ── Error screen ───────────────────────────────────────────
3316
+ renderError(err) {
3317
+ if (!this.dialog) return;
3318
+ this.dialog.innerHTML = "";
3319
+ const title = document.createElement("div");
3320
+ title.className = "atel-export-dialog__title";
3321
+ title.textContent = "Export Failed";
3322
+ this.dialog.appendChild(title);
3323
+ const msg = document.createElement("div");
3324
+ msg.className = "atel-export-dialog__info";
3325
+ msg.textContent = err instanceof Error ? err.message : String(err);
3326
+ this.dialog.appendChild(msg);
3327
+ const closeBtn = document.createElement("button");
3328
+ closeBtn.className = "atel-toolbar__btn";
3329
+ closeBtn.textContent = "Close";
3330
+ closeBtn.style.marginTop = "8px";
3331
+ closeBtn.addEventListener("click", () => this.destroy());
3332
+ this.dialog.appendChild(closeBtn);
3333
+ }
3334
+ };
3335
+
3336
+ // src/studio.ts
3337
+ var AtelierStudio = class {
3338
+ constructor(container, opts = {}) {
3339
+ this.container = container;
3340
+ this.opts = opts;
3341
+ this.currentTab = opts.initialTab ?? "yaml";
3342
+ this.root = document.createElement("div");
3343
+ this.root.className = "atel-studio";
3344
+ applyStudioTheme(this.root, defaultTheme);
3345
+ this.toolbar = document.createElement("div");
3346
+ this.toolbar.className = "atel-toolbar";
3347
+ this.buildToolbar();
3348
+ this.root.appendChild(this.toolbar);
3349
+ this.body = document.createElement("div");
3350
+ this.body.className = "atel-body";
3351
+ this.root.appendChild(this.body);
3352
+ this.rightPanel = document.createElement("div");
3353
+ this.rightPanel.className = "atel-right-panel";
3354
+ this.canvasPanel = new CanvasPanel(this.body, {
3355
+ onFrameChange: () => {
3356
+ }
3357
+ });
3358
+ this.body.appendChild(this.rightPanel);
3359
+ this.layerPanel = new LayerPanel(this.rightPanel, {
3360
+ onSelectLayer: (id) => {
3361
+ this.propertyPanel.selectLayer(id);
3362
+ },
3363
+ onDocumentChange: () => this.onDocChange()
3364
+ });
3365
+ this.propertyPanel = new PropertyPanel(this.rightPanel, {
3366
+ onPropertyChange: () => this.onDocChange()
3367
+ });
3368
+ this.yamlPanel = new YamlPanel(this.rightPanel);
3369
+ container.appendChild(this.root);
3370
+ this.applyTab();
3371
+ this.root.tabIndex = 0;
3372
+ this.root.addEventListener("keydown", (e) => this.handleKeydown(e), {
3373
+ signal: this.abortController.signal
3374
+ });
3375
+ }
3376
+ root;
3377
+ toolbar;
3378
+ body;
3379
+ rightPanel;
3380
+ stateSelect;
3381
+ tabBtns = /* @__PURE__ */ new Map();
3382
+ undoBtn;
3383
+ redoBtn;
3384
+ canvasPanel;
3385
+ layerPanel;
3386
+ propertyPanel;
3387
+ yamlPanel;
3388
+ doc = null;
3389
+ originalDoc = null;
3390
+ currentTab;
3391
+ stateName = "";
3392
+ abortController = new AbortController();
3393
+ history = new History();
3394
+ lastSnapshot = "";
3395
+ handleKeydown(e) {
3396
+ const mod = e.metaKey || e.ctrlKey;
3397
+ if (mod && e.key === "z" && !e.shiftKey) {
3398
+ e.preventDefault();
3399
+ this.undoAction();
3400
+ return;
3401
+ }
3402
+ if (mod && e.key === "z" && e.shiftKey) {
3403
+ e.preventDefault();
3404
+ this.redoAction();
3405
+ return;
3406
+ }
3407
+ if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement || e.target instanceof HTMLSelectElement) return;
3408
+ switch (e.key) {
3409
+ case " ":
3410
+ e.preventDefault();
3411
+ this.canvasPanel.togglePlay();
3412
+ break;
3413
+ case "ArrowRight":
3414
+ e.preventDefault();
3415
+ this.canvasPanel.stepForward();
3416
+ break;
3417
+ case "ArrowLeft":
3418
+ e.preventDefault();
3419
+ this.canvasPanel.stepBack();
3420
+ break;
3421
+ case "Delete":
3422
+ case "Backspace":
3423
+ e.preventDefault();
3424
+ this.layerPanel.removeSelectedLayer();
3425
+ this.onDocChange();
3426
+ break;
3427
+ }
3428
+ }
3429
+ loadDocument(doc) {
3430
+ this.originalDoc = structuredClone(doc);
3431
+ this.doc = structuredClone(doc);
3432
+ this.stateName = Object.keys(this.doc.states)[0] ?? "";
3433
+ this.lastSnapshot = JSON.stringify(this.doc);
3434
+ this.history.clear();
3435
+ this.stateSelect.innerHTML = "";
3436
+ const stateNames = Object.keys(this.doc.states);
3437
+ for (const name of stateNames) {
3438
+ const o = document.createElement("option");
3439
+ o.value = name;
3440
+ o.textContent = name;
3441
+ this.stateSelect.appendChild(o);
3442
+ }
3443
+ this.stateSelect.value = this.stateName;
3444
+ this.stateSelect.style.display = stateNames.length > 1 ? "" : "none";
3445
+ this.canvasPanel.loadDocument(this.doc, this.stateName);
3446
+ this.layerPanel.loadDocument(this.doc);
3447
+ this.propertyPanel.loadDocument(this.doc, this.stateName);
3448
+ this.yamlPanel.loadDocument(this.doc);
3449
+ }
3450
+ getDocument() {
3451
+ return this.doc ? structuredClone(this.doc) : null;
3452
+ }
3453
+ setTheme(theme) {
3454
+ applyStudioTheme(this.root, theme);
3455
+ }
3456
+ setTab(tab) {
3457
+ this.currentTab = tab;
3458
+ this.applyTab();
3459
+ }
3460
+ destroy() {
3461
+ this.abortController.abort();
3462
+ this.canvasPanel.destroy();
3463
+ this.container.removeChild(this.root);
3464
+ }
3465
+ buildToolbar() {
3466
+ const leftGroup = document.createElement("div");
3467
+ leftGroup.className = "atel-toolbar__group atel-toolbar__group--controls";
3468
+ this.stateSelect = document.createElement("select");
3469
+ this.stateSelect.className = "atel-toolbar__select";
3470
+ this.stateSelect.style.display = "none";
3471
+ this.stateSelect.addEventListener("change", () => {
3472
+ this.stateName = this.stateSelect.value;
3473
+ this.canvasPanel.setStateName(this.stateName);
3474
+ this.propertyPanel.setStateName(this.stateName);
3475
+ });
3476
+ leftGroup.appendChild(this.stateSelect);
3477
+ const resetBtn = document.createElement("button");
3478
+ resetBtn.className = "atel-toolbar__btn";
3479
+ resetBtn.textContent = "Reset";
3480
+ resetBtn.dataset.action = "reset";
3481
+ resetBtn.addEventListener("click", () => this.resetDocument());
3482
+ leftGroup.appendChild(resetBtn);
3483
+ this.undoBtn = document.createElement("button");
3484
+ this.undoBtn.className = "atel-toolbar__btn";
3485
+ this.undoBtn.textContent = "\u21A9 Undo";
3486
+ this.undoBtn.dataset.action = "undo";
3487
+ this.undoBtn.disabled = true;
3488
+ this.undoBtn.addEventListener("click", () => this.undoAction());
3489
+ leftGroup.appendChild(this.undoBtn);
3490
+ this.redoBtn = document.createElement("button");
3491
+ this.redoBtn.className = "atel-toolbar__btn";
3492
+ this.redoBtn.textContent = "Redo \u21AA";
3493
+ this.redoBtn.dataset.action = "redo";
3494
+ this.redoBtn.disabled = true;
3495
+ this.redoBtn.addEventListener("click", () => this.redoAction());
3496
+ leftGroup.appendChild(this.redoBtn);
3497
+ this.toolbar.appendChild(leftGroup);
3498
+ const spacer = document.createElement("div");
3499
+ spacer.className = "atel-toolbar__spacer";
3500
+ this.toolbar.appendChild(spacer);
3501
+ const exportGroup = document.createElement("div");
3502
+ exportGroup.className = "atel-toolbar__group--controls";
3503
+ const exportBtn = document.createElement("button");
3504
+ exportBtn.className = "atel-toolbar__btn";
3505
+ exportBtn.textContent = "Export";
3506
+ exportBtn.dataset.action = "export";
3507
+ exportBtn.addEventListener("click", () => this.showExportDialog());
3508
+ exportGroup.appendChild(exportBtn);
3509
+ this.toolbar.appendChild(exportGroup);
3510
+ const tabGroup = document.createElement("div");
3511
+ tabGroup.className = "atel-toolbar__group";
3512
+ const tabs = [
3513
+ { id: "yaml", label: "{ } YAML" },
3514
+ { id: "editor", label: "\u270E Editor" }
3515
+ ];
3516
+ for (const t of tabs) {
3517
+ const btn = document.createElement("button");
3518
+ btn.className = "atel-toolbar__btn";
3519
+ btn.textContent = t.label;
3520
+ btn.addEventListener("click", () => this.setTab(t.id));
3521
+ this.tabBtns.set(t.id, btn);
3522
+ tabGroup.appendChild(btn);
3523
+ }
3524
+ this.toolbar.appendChild(tabGroup);
3525
+ }
3526
+ applyTab() {
3527
+ for (const [id, btn] of this.tabBtns) {
3528
+ btn.classList.toggle("atel-toolbar__btn--active", id === this.currentTab);
3529
+ }
3530
+ this.layerPanel.el.style.display = "none";
3531
+ this.propertyPanel.el.style.display = "none";
3532
+ this.yamlPanel.el.style.display = "none";
3533
+ this.rightPanel.className = "atel-right-panel";
3534
+ switch (this.currentTab) {
3535
+ case "yaml":
3536
+ this.rightPanel.classList.add("atel-right-panel--yaml");
3537
+ this.yamlPanel.el.style.display = "";
3538
+ this.yamlPanel.refresh();
3539
+ break;
3540
+ case "editor":
3541
+ this.rightPanel.classList.add("atel-right-panel--editor");
3542
+ this.layerPanel.el.style.display = "";
3543
+ this.propertyPanel.el.style.display = "";
3544
+ break;
3545
+ }
3546
+ }
3547
+ onDocChange() {
3548
+ if (this.doc) {
3549
+ this.history.snapshot(this.lastSnapshot);
3550
+ this.lastSnapshot = JSON.stringify(this.doc);
3551
+ }
3552
+ this.canvasPanel.refresh();
3553
+ if (this.currentTab === "yaml") {
3554
+ this.yamlPanel.refresh();
3555
+ }
3556
+ this.layerPanel.refresh();
3557
+ this.updateUndoRedoButtons();
3558
+ if (this.doc) {
3559
+ this.opts.onDocumentChange?.(this.doc);
3560
+ }
3561
+ }
3562
+ undoAction() {
3563
+ if (!this.doc) return;
3564
+ const json = this.history.undo(this.lastSnapshot);
3565
+ if (json === null) return;
3566
+ this.lastSnapshot = json;
3567
+ this.doc = JSON.parse(json);
3568
+ this.reloadPanels();
3569
+ this.updateUndoRedoButtons();
3570
+ }
3571
+ redoAction() {
3572
+ if (!this.doc) return;
3573
+ const json = this.history.redo(this.lastSnapshot);
3574
+ if (json === null) return;
3575
+ this.lastSnapshot = json;
3576
+ this.doc = JSON.parse(json);
3577
+ this.reloadPanels();
3578
+ this.updateUndoRedoButtons();
3579
+ }
3580
+ updateUndoRedoButtons() {
3581
+ this.undoBtn.disabled = !this.history.canUndo;
3582
+ this.redoBtn.disabled = !this.history.canRedo;
3583
+ }
3584
+ reloadPanels() {
3585
+ if (!this.doc) return;
3586
+ this.stateName = Object.keys(this.doc.states)[0] ?? "";
3587
+ this.canvasPanel.loadDocument(this.doc, this.stateName);
3588
+ this.layerPanel.loadDocument(this.doc);
3589
+ this.propertyPanel.loadDocument(this.doc, this.stateName);
3590
+ this.yamlPanel.loadDocument(this.doc);
3591
+ if (this.currentTab === "yaml") this.yamlPanel.refresh();
3592
+ this.opts.onDocumentChange?.(this.doc);
3593
+ }
3594
+ resetDocument() {
3595
+ if (this.originalDoc) {
3596
+ this.loadDocument(this.originalDoc);
3597
+ }
3598
+ }
3599
+ showExportDialog() {
3600
+ if (!this.doc) return;
3601
+ this.canvasPanel.pause();
3602
+ const dialog = new ExportDialog();
3603
+ dialog.show(
3604
+ this.doc,
3605
+ this.canvasPanel.getCanvas(),
3606
+ this.canvasPanel.getImageCache(),
3607
+ this.root
3608
+ );
3609
+ }
3610
+ };
3611
+ // Annotate the CommonJS export names for ESM import in node:
3612
+ 0 && (module.exports = {
3613
+ AtelierStudio,
3614
+ applyStudioTheme,
3615
+ defaultTheme,
3616
+ exportDocument,
3617
+ supportsGifExport,
3618
+ supportsWebCodecs,
3619
+ supportsWebM
3620
+ });
3621
+ //# sourceMappingURL=index.cjs.map