@a-company/atelier 0.28.2 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -4,20 +4,119 @@ import {
4
4
  exportLottieCommand,
5
5
  exportSvgCommand,
6
6
  infoCommand,
7
+ parseAtelier,
7
8
  renderCommand,
8
9
  stillCommand,
9
10
  validateCommand,
11
+ validateVideoLayer,
10
12
  variablesCommand
11
- } from "./chunk-LC7ICNMN.js";
12
- import "./chunk-C5DBTHXB.js";
13
+ } from "./chunk-JV7RGETS.js";
14
+ import {
15
+ validateAllDeltas
16
+ } from "./chunk-JPZ4F4PW.js";
13
17
 
14
18
  // src/cli.ts
15
19
  import { createRequire } from "module";
16
20
  import { Command } from "commander";
17
21
 
22
+ // src/commands/lint.ts
23
+ import { readFileSync } from "fs";
24
+ import { resolve } from "path";
25
+ function lintFile(filePath) {
26
+ const absPath = resolve(filePath);
27
+ let content;
28
+ try {
29
+ content = readFileSync(absPath, "utf-8");
30
+ } catch {
31
+ return {
32
+ file: absPath,
33
+ valid: false,
34
+ gates: [
35
+ {
36
+ gate: "^valid-document",
37
+ pass: false,
38
+ errors: [`Cannot read file: ${absPath}`]
39
+ }
40
+ ]
41
+ };
42
+ }
43
+ const gates = [];
44
+ const parseResult = parseAtelier(content);
45
+ if (!parseResult.success) {
46
+ gates.push({
47
+ gate: "^valid-document",
48
+ pass: false,
49
+ errors: parseResult.errors.map((e) => `${e.path}: ${e.message}`)
50
+ });
51
+ return { file: absPath, valid: false, gates };
52
+ }
53
+ gates.push({ gate: "^valid-document", pass: true, errors: [] });
54
+ const doc = parseResult.data;
55
+ const deltaErrors = [];
56
+ for (const [stateName, state] of Object.entries(doc.states)) {
57
+ const overlaps = validateAllDeltas(state.deltas);
58
+ for (const overlap of overlaps) {
59
+ deltaErrors.push(`State "${stateName}": ${overlap.message}`);
60
+ }
61
+ }
62
+ gates.push({
63
+ gate: "^valid-delta",
64
+ pass: deltaErrors.length === 0,
65
+ errors: deltaErrors
66
+ });
67
+ const videoErrors = [];
68
+ for (const layer of doc.layers) {
69
+ if (layer.visual.type !== "video") continue;
70
+ const visual = layer.visual;
71
+ const duration = doc.assets?.[visual.assetId]?.videoMeta?.duration;
72
+ const result = validateVideoLayer(visual, duration);
73
+ if (!result.success) {
74
+ for (const err of result.errors) {
75
+ videoErrors.push(`Layer "${layer.id}" (${err.path}): ${err.message}`);
76
+ }
77
+ }
78
+ }
79
+ gates.push({
80
+ gate: "^valid-video-layer",
81
+ pass: videoErrors.length === 0,
82
+ errors: videoErrors
83
+ });
84
+ const valid = gates.every((g) => g.pass);
85
+ return { file: absPath, valid, gates };
86
+ }
87
+ function formatResult(result) {
88
+ const lines = [];
89
+ const status = result.valid ? "PASS" : "FAIL";
90
+ lines.push(`${status} ${result.file}`);
91
+ for (const gate of result.gates) {
92
+ const gateStatus = gate.pass ? " \u2713" : " \u2717";
93
+ lines.push(`${gateStatus} ${gate.gate}`);
94
+ for (const err of gate.errors) {
95
+ lines.push(` ${err}`);
96
+ }
97
+ }
98
+ return lines.join("\n");
99
+ }
100
+ function lintCommand(program2) {
101
+ program2.command("lint <files...>").description(
102
+ "Lint .atelier files against all gates (^valid-document, ^valid-delta, ^valid-video-layer)"
103
+ ).option("--json", "Output results as JSON array").action((files, opts) => {
104
+ const results = files.map(lintFile);
105
+ if (opts.json) {
106
+ console.log(JSON.stringify(results, null, 2));
107
+ } else {
108
+ for (const result of results) {
109
+ console.log(formatResult(result));
110
+ }
111
+ }
112
+ const allValid = results.every((r) => r.valid);
113
+ if (!allValid) process.exit(1);
114
+ });
115
+ }
116
+
18
117
  // src/commands/studio.ts
19
- import { resolve, join, relative, dirname } from "path";
20
- import { mkdirSync, writeFileSync, rmSync, readFileSync, readdirSync, statSync, realpathSync, symlinkSync, existsSync } from "fs";
118
+ import { resolve as resolve2, join, relative, dirname } from "path";
119
+ import { mkdirSync, writeFileSync, rmSync, readFileSync as readFileSync2, readdirSync, statSync, realpathSync, symlinkSync, existsSync } from "fs";
21
120
  import { tmpdir } from "os";
22
121
  import { randomBytes } from "crypto";
23
122
  import { exec } from "child_process";
@@ -48,7 +147,7 @@ function findAtelierFiles(dir, base = dir) {
48
147
  }
49
148
  function isSafePath(filePath) {
50
149
  if (!filePath || filePath.includes("..") || filePath.startsWith("/")) return false;
51
- const resolved = resolve(process.cwd(), filePath);
150
+ const resolved = resolve2(process.cwd(), filePath);
52
151
  return resolved.startsWith(process.cwd());
53
152
  }
54
153
  function getInlineHTML() {
@@ -502,7 +601,7 @@ function studioCommand(program2) {
502
601
  process.exit(1);
503
602
  }
504
603
  const cwd = process.cwd();
505
- const cliPackageDir = resolve(dirname(new URL(import.meta.url).pathname), "..");
604
+ const cliPackageDir = resolve2(dirname(new URL(import.meta.url).pathname), "..");
506
605
  const tmpId = randomBytes(4).toString("hex");
507
606
  const tmpDirRaw = join(tmpdir(), `atelier-studio-${tmpId}`);
508
607
  mkdirSync(tmpDirRaw, { recursive: true });
@@ -563,10 +662,10 @@ function studioCommand(program2) {
563
662
  res.end("Invalid path");
564
663
  return;
565
664
  }
566
- const absPath = resolve(cwd, filePath);
665
+ const absPath = resolve2(cwd, filePath);
567
666
  if (req.method === "GET") {
568
667
  try {
569
- const content = readFileSync(absPath, "utf-8");
668
+ const content = readFileSync2(absPath, "utf-8");
570
669
  res.setHeader("Content-Type", "text/plain");
571
670
  res.end(content);
572
671
  } catch {
@@ -599,7 +698,7 @@ function studioCommand(program2) {
599
698
  res.end("Invalid path");
600
699
  return;
601
700
  }
602
- const absPath = resolve(cwd, filePath);
701
+ const absPath = resolve2(cwd, filePath);
603
702
  const chunks = [];
604
703
  req.on("data", (chunk) => {
605
704
  chunks.push(chunk);
@@ -661,6 +760,7 @@ function studioCommand(program2) {
661
760
  var program = new Command();
662
761
  program.name("atelier").description("Atelier animation CLI").version(createRequire(import.meta.url)("../package.json").version);
663
762
  validateCommand(program);
763
+ lintCommand(program);
664
764
  infoCommand(program);
665
765
  stillCommand(program);
666
766
  renderCommand(program);
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cli.ts","../src/commands/studio.ts"],"sourcesContent":["#!/usr/bin/env node\n// @a-company/atelier-cli — Entry point for the `atelier` command\n\nimport { createRequire } from \"node:module\";\nimport { Command } from \"commander\";\nimport { validateCommand } from \"./commands/validate.js\";\nimport { infoCommand } from \"./commands/info.js\";\nimport { stillCommand } from \"./commands/still.js\";\nimport { renderCommand } from \"./commands/render.js\";\nimport { exportSvgCommand } from \"./commands/export-svg.js\";\nimport { exportLottieCommand } from \"./commands/export-lottie.js\";\nimport { assetsCommand } from \"./commands/assets.js\";\nimport { variablesCommand } from \"./commands/variables.js\";\nimport { studioCommand } from \"./commands/studio.js\";\n\nconst program = new Command();\n\nprogram\n .name(\"atelier\")\n .description(\"Atelier animation CLI\")\n .version(createRequire(import.meta.url)(\"../package.json\").version);\n\n// Register commands\nvalidateCommand(program);\ninfoCommand(program);\nstillCommand(program);\nrenderCommand(program);\nexportSvgCommand(program);\nexportLottieCommand(program);\nassetsCommand(program);\nvariablesCommand(program);\nstudioCommand(program);\n\nprogram.parse();\n","/**\n * `atelier studio [file]` — launch the browser-based Atelier editor.\n *\n * Spins up a Vite dev server with a temporary app that imports AtelierStudio,\n * provides a file API for reading/writing .atelier files from CWD, and opens\n * the browser.\n *\n * Usage:\n * atelier studio → browse all .atelier files in CWD\n * atelier studio my-animation.atelier → open specific file\n * atelier studio --port 8080 → custom port\n * atelier studio --no-open → don't auto-open browser\n */\n\nimport { resolve, join, relative, dirname } from \"node:path\";\nimport { mkdirSync, writeFileSync, rmSync, readFileSync, readdirSync, statSync, realpathSync, symlinkSync, existsSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { randomBytes } from \"node:crypto\";\nimport { exec } from \"node:child_process\";\nimport type { Command } from \"commander\";\n\n/** Recursively glob for .atelier files under a directory. */\nfunction findAtelierFiles(dir: string, base: string = dir): string[] {\n const results: string[] = [];\n let entries: string[];\n try {\n entries = readdirSync(dir);\n } catch {\n return results;\n }\n for (const entry of entries) {\n if (entry === \"node_modules\" || entry === \"dist\" || entry === \".git\") continue;\n const full = join(dir, entry);\n let stat;\n try {\n stat = statSync(full);\n } catch {\n continue;\n }\n if (stat.isDirectory()) {\n results.push(...findAtelierFiles(full, base));\n } else if (entry.endsWith(\".atelier\")) {\n results.push(relative(base, full));\n }\n }\n return results.sort();\n}\n\n/** Validate that a file path is safe (relative, no traversal). */\nfunction isSafePath(filePath: string): boolean {\n if (!filePath || filePath.includes(\"..\") || filePath.startsWith(\"/\")) return false;\n const resolved = resolve(process.cwd(), filePath);\n return resolved.startsWith(process.cwd());\n}\n\nfunction getInlineHTML(): string {\n return `<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <title>Atelier Studio</title>\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link href=\"https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400&display=swap\" rel=\"stylesheet\">\n </head>\n <body>\n <div id=\"studio\"></div>\n <script type=\"module\" src=\"/main.ts\"></script>\n </body>\n</html>`;\n}\n\nfunction getInlineApp(initialFile: string | null): string {\n const initialFileStr = initialFile ? JSON.stringify(initialFile) : \"null\";\n return `import { AtelierStudio, exportDocument, ImageCache } from \"@a-company/atelier-studio\";\nimport \"@a-company/atelier-studio/styles.css\";\nimport { parseAtelier, serializeAtelier } from \"@a-company/atelier-schema\";\n\n// ── Types ──\ninterface FileEntry {\n path: string;\n name: string;\n folder: string;\n}\n\n// ── State ──\nlet studio: AtelierStudio | null = null;\nlet currentFile: string | null = null;\nlet files: FileEntry[] = [];\nlet saveTimeout: ReturnType<typeof setTimeout> | null = null;\n\n// ── API helpers ──\nasync function fetchFiles(): Promise<FileEntry[]> {\n const res = await fetch(\"/api/files\");\n return res.json();\n}\n\nasync function fetchFileContent(path: string): Promise<string> {\n const res = await fetch(\"/api/file?path=\" + encodeURIComponent(path));\n return res.text();\n}\n\nasync function saveFileContent(path: string, content: string): Promise<void> {\n await fetch(\"/api/file?path=\" + encodeURIComponent(path), {\n method: \"POST\",\n headers: { \"Content-Type\": \"text/plain\" },\n body: content,\n });\n}\n\nasync function saveExportBlob(path: string, blob: Blob): Promise<void> {\n const buf = await blob.arrayBuffer();\n await fetch(\"/api/export?path=\" + encodeURIComponent(path), {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/octet-stream\" },\n body: buf,\n });\n}\n\nasync function exportAll(format: \"gif\" | \"mp4\" | \"webm\"): Promise<void> {\n if (files.length === 0) return;\n\n // Create progress overlay\n const overlay = document.createElement(\"div\");\n overlay.style.cssText = \"position:fixed;inset:0;background:rgba(0,0,0,0.75);display:flex;align-items:center;justify-content:center;z-index:10000\";\n const card = document.createElement(\"div\");\n card.style.cssText = \"background:#333;border:1px solid #4A4A4A;border-radius:8px;padding:32px 40px;min-width:360px;color:#F5F0EB;font-family:'Cormorant Garamond',Georgia,serif\";\n overlay.appendChild(card);\n document.body.appendChild(overlay);\n\n const title = document.createElement(\"div\");\n title.style.cssText = \"font-size:18px;margin-bottom:16px;font-weight:600\";\n title.textContent = \"Exporting All Files…\";\n card.appendChild(title);\n\n const fileLabel = document.createElement(\"div\");\n fileLabel.style.cssText = \"font-size:13px;color:#A89F95;margin-bottom:8px;font-family:'SF Mono','Fira Code',monospace\";\n card.appendChild(fileLabel);\n\n const progress = document.createElement(\"progress\");\n progress.style.cssText = \"width:100%;height:6px;appearance:none;-webkit-appearance:none\";\n progress.max = files.length;\n progress.value = 0;\n card.appendChild(progress);\n\n const statusText = document.createElement(\"div\");\n statusText.style.cssText = \"font-size:12px;color:#A89F95;margin-top:8px\";\n card.appendChild(statusText);\n\n let exported = 0;\n let errors = 0;\n\n for (const file of files) {\n fileLabel.textContent = file.path;\n statusText.textContent = (exported + errors + 1) + \" / \" + files.length;\n\n try {\n const content = await fetchFileContent(file.path);\n const result = parseAtelier(content);\n if (!result.success) {\n errors++;\n progress.value = exported + errors;\n continue;\n }\n\n const doc = result.data;\n const w = doc.canvas.width;\n const h = doc.canvas.height;\n const canvas = document.createElement(\"canvas\");\n canvas.width = w;\n canvas.height = h;\n const imageCache = new ImageCache();\n\n const exportResult = await exportDocument(doc, canvas, imageCache, {\n format,\n onProgress: ({ percent }) => {\n statusText.textContent = (exported + errors + 1) + \" / \" + files.length + \" — \" + percent + \"%\";\n },\n });\n\n // Save alongside the source file: e.g. \"dir/my-anim.atelier\" → \"dir/my-anim.gif\"\n const outPath = file.path.replace(/\\\\.atelier$/, \".\" + format);\n await saveExportBlob(outPath, exportResult.blob);\n exported++;\n } catch (e) {\n console.error(\"Export failed:\", file.path, e);\n errors++;\n }\n progress.value = exported + errors;\n }\n\n // Done\n title.textContent = \"Export Complete\";\n fileLabel.textContent = \"\";\n statusText.textContent = exported + \" exported\" + (errors > 0 ? \", \" + errors + \" failed\" : \"\");\n if (errors > 0) console.warn(\"Export All finished with \" + errors + \" error(s). Check console for details.\");\n\n const closeBtn = document.createElement(\"button\");\n closeBtn.style.cssText = \"margin-top:16px;padding:6px 20px;background:#3D3D3D;color:#F5F0EB;border:1px solid #4A4A4A;border-radius:4px;cursor:pointer;font-family:inherit;font-size:13px\";\n closeBtn.textContent = \"Close\";\n closeBtn.addEventListener(\"click\", () => document.body.removeChild(overlay));\n card.appendChild(closeBtn);\n}\n\n// ── Theme (matches branded theme from showcase) ──\nconst theme = {\n bg: \"#2C2C2C\",\n bgSecondary: \"#333333\",\n bgTertiary: \"#3D3D3D\",\n text: \"#F5F0EB\",\n textMuted: \"#A89F95\",\n textAccent: \"#F5F0EB\",\n border: \"#4A4A4A\",\n buttonBg: \"#3D3D3D\",\n buttonHover: \"#4A4A4A\",\n buttonActive: \"#555555\",\n accent: \"#C75B39\",\n accentHover: \"#D4724E\",\n sliderTrack: \"#4A4A4A\",\n sliderThumb: \"#C75B39\",\n fontFamily: \"'Cormorant Garamond', Georgia, serif\",\n fontMono: \"'SF Mono', 'Fira Code', monospace\",\n canvasShadow: \"0 4px 60px rgba(199, 91, 57, 0.12), 0 0 40px rgba(0,0,0,0.4)\",\n};\n\n// ── Styles ──\nconst style = document.createElement(\"style\");\nstyle.textContent = \\`\n * { margin: 0; padding: 0; box-sizing: border-box; }\n html, body { height: 100%; overflow: hidden; background: #2C2C2C; color: #F5F0EB; }\n body { font-family: 'Cormorant Garamond', Georgia, serif; }\n #studio { display: flex; height: 100vh; width: 100vw; }\n\n .sidebar {\n width: 260px;\n min-width: 260px;\n background: #333333;\n border-right: 1px solid #4A4A4A;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n }\n .sidebar__header {\n padding: 16px 20px;\n border-bottom: 1px solid #4A4A4A;\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 2px;\n text-transform: uppercase;\n color: #A89F95;\n display: flex;\n align-items: center;\n gap: 8px;\n }\n .sidebar__header span {\n color: #C75B39;\n font-size: 13px;\n }\n .sidebar__list {\n flex: 1;\n overflow-y: auto;\n padding: 8px 0;\n }\n .sidebar__list::-webkit-scrollbar { width: 6px; }\n .sidebar__list::-webkit-scrollbar-track { background: transparent; }\n .sidebar__list::-webkit-scrollbar-thumb { background: #4A4A4A; border-radius: 3px; }\n\n .sidebar__folder {\n padding: 10px 20px 4px;\n font-size: 10px;\n font-weight: 600;\n letter-spacing: 1.5px;\n text-transform: uppercase;\n color: #A89F95;\n }\n .sidebar__item {\n padding: 8px 20px 8px 28px;\n font-size: 13px;\n cursor: pointer;\n color: #A89F95;\n transition: background 0.15s, color 0.15s;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n font-family: 'SF Mono', 'Fira Code', monospace;\n font-size: 11.5px;\n }\n .sidebar__item:hover { background: #363636; color: #F5F0EB; }\n .sidebar__item--active {\n background: rgba(199, 91, 57, 0.12) !important;\n color: #C75B39 !important;\n }\n\n .main {\n flex: 1;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n }\n .main__status {\n height: 32px;\n min-height: 32px;\n display: flex;\n align-items: center;\n padding: 0 16px;\n background: #333333;\n border-bottom: 1px solid #4A4A4A;\n font-size: 11px;\n color: #A89F95;\n font-family: 'SF Mono', 'Fira Code', monospace;\n gap: 12px;\n }\n .main__status .save-indicator {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n margin-left: auto;\n transition: opacity 0.3s;\n }\n .main__status .save-indicator--saving { color: #C75B39; }\n .main__status .save-indicator--saved { color: #6B8E6B; }\n .main__editor {\n flex: 1;\n overflow: hidden;\n }\n .main__empty {\n flex: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n color: #A89F95;\n font-size: 18px;\n }\n\\`;\ndocument.head.appendChild(style);\n\n// ── Build UI ──\nconst root = document.getElementById(\"studio\")!;\nconst sidebar = document.createElement(\"div\");\nsidebar.className = \"sidebar\";\n\nconst sidebarHeader = document.createElement(\"div\");\nsidebarHeader.className = \"sidebar__header\";\nsidebarHeader.innerHTML = '<span>&#9670;</span> ATELIER STUDIO';\nsidebar.appendChild(sidebarHeader);\n\nconst sidebarList = document.createElement(\"div\");\nsidebarList.className = \"sidebar__list\";\nsidebar.appendChild(sidebarList);\n\nconst sidebarFooter = document.createElement(\"div\");\nsidebarFooter.style.cssText = \"padding:12px 16px;border-top:1px solid #4A4A4A;display:flex;gap:8px;align-items:center\";\nconst exportAllSelect = document.createElement(\"select\");\nexportAllSelect.style.cssText = \"flex:1;background:#3D3D3D;color:#F5F0EB;border:1px solid #4A4A4A;border-radius:4px;padding:4px 8px;font-size:11px;font-family:'SF Mono','Fira Code',monospace;cursor:pointer\";\nfor (const [val, label] of [[\"gif\",\"GIF\"],[\"mp4\",\"MP4\"],[\"webm\",\"WebM\"]] as const) {\n const o = document.createElement(\"option\");\n o.value = val;\n o.textContent = label;\n exportAllSelect.appendChild(o);\n}\nsidebarFooter.appendChild(exportAllSelect);\nconst exportAllBtn = document.createElement(\"button\");\nexportAllBtn.style.cssText = \"background:#C75B39;color:#F5F0EB;border:none;border-radius:4px;padding:5px 12px;font-size:11px;font-family:inherit;cursor:pointer;white-space:nowrap\";\nexportAllBtn.textContent = \"Export All\";\nexportAllBtn.addEventListener(\"click\", () => {\n exportAll(exportAllSelect.value as \"gif\" | \"mp4\" | \"webm\");\n});\nsidebarFooter.appendChild(exportAllBtn);\nsidebar.appendChild(sidebarFooter);\n\nconst main = document.createElement(\"div\");\nmain.className = \"main\";\n\nconst statusBar = document.createElement(\"div\");\nstatusBar.className = \"main__status\";\nmain.appendChild(statusBar);\n\nconst editorContainer = document.createElement(\"div\");\neditorContainer.className = \"main__editor\";\nmain.appendChild(editorContainer);\n\nroot.appendChild(sidebar);\nroot.appendChild(main);\n\n// ── File list rendering ──\nfunction renderFileList(): void {\n sidebarList.innerHTML = \"\";\n let lastFolder = \"\";\n\n for (const file of files) {\n if (file.folder && file.folder !== lastFolder) {\n lastFolder = file.folder;\n const folder = document.createElement(\"div\");\n folder.className = \"sidebar__folder\";\n folder.textContent = file.folder;\n sidebarList.appendChild(folder);\n }\n\n const item = document.createElement(\"div\");\n item.className = \"sidebar__item\" + (file.path === currentFile ? \" sidebar__item--active\" : \"\");\n item.textContent = file.name;\n item.title = file.path;\n item.addEventListener(\"click\", () => loadFile(file.path));\n sidebarList.appendChild(item);\n }\n}\n\n// ── Load a file into the studio ──\nasync function loadFile(path: string): Promise<void> {\n currentFile = path;\n renderFileList();\n\n const content = await fetchFileContent(path);\n const result = parseAtelier(content);\n\n if (!result.success) {\n editorContainer.innerHTML = \"\";\n const err = document.createElement(\"div\");\n err.className = \"main__empty\";\n err.style.flexDirection = \"column\";\n err.style.gap = \"8px\";\n err.innerHTML = '<div style=\"color:#C75B39\">Parse Error</div><div style=\"font-size:13px;font-family:monospace\">' +\n result.errors.map(e => e.path + \": \" + e.message).join(\"<br>\") + \"</div>\";\n editorContainer.appendChild(err);\n return;\n }\n\n statusBar.innerHTML = '<span>' + path + '</span><span class=\"save-indicator save-indicator--saved\">&#10003; saved</span>';\n\n if (studio) {\n studio.destroy();\n studio = null;\n }\n\n // Set filename for export downloads (strip path and .atelier extension)\n const baseName = path.split(\"/\").pop()?.replace(/\\\\.atelier$/, \"\") || null;\n\n studio = new AtelierStudio(editorContainer, {\n mode: \"full\",\n initialTab: \"yaml\",\n allowSave: true,\n onDocumentChange: (doc) => {\n // Auto-save with debounce\n const indicator = statusBar.querySelector(\".save-indicator\");\n if (indicator) {\n indicator.className = \"save-indicator save-indicator--saving\";\n indicator.innerHTML = \"&#9679; saving...\";\n }\n\n if (saveTimeout) clearTimeout(saveTimeout);\n saveTimeout = setTimeout(async () => {\n if (!currentFile) return;\n const yaml = serializeAtelier(doc);\n await saveFileContent(currentFile, yaml);\n const ind = statusBar.querySelector(\".save-indicator\");\n if (ind) {\n ind.className = \"save-indicator save-indicator--saved\";\n ind.innerHTML = \"&#10003; saved\";\n }\n }, 800);\n },\n });\n studio.setTheme(theme);\n studio.setFilename(baseName);\n studio.loadDocument(result.data);\n}\n\n// ── Boot ──\nasync function boot(): Promise<void> {\n files = await fetchFiles();\n\n if (files.length === 0) {\n editorContainer.innerHTML = \"\";\n const empty = document.createElement(\"div\");\n empty.className = \"main__empty\";\n empty.textContent = \"No .atelier files found in this directory\";\n editorContainer.appendChild(empty);\n statusBar.textContent = \"No files\";\n renderFileList();\n return;\n }\n\n renderFileList();\n\n const initialFile = ${initialFileStr};\n const target = initialFile\n ? files.find(f => f.path === initialFile || f.path.endsWith(initialFile))\n : files[0];\n\n if (target) {\n await loadFile(target.path);\n }\n}\n\nboot();\n`;\n}\n\n/** Register the `studio` subcommand on the Commander program. */\nexport function studioCommand(program: Command): void {\n program\n .command(\"studio [file]\")\n .description(\"Launch the browser-based Atelier editor\")\n .option(\"-p, --port <number>\", \"Port to serve on\", \"4321\")\n .option(\"--no-open\", \"Don't auto-open browser\")\n .action(\n async (\n file: string | undefined,\n options: { port: string; open: boolean },\n ) => {\n const port = parseInt(options.port, 10);\n if (isNaN(port) || port < 1 || port > 65535) {\n console.error(`Invalid port: ${options.port}`);\n process.exit(1);\n }\n\n const cwd = process.cwd();\n\n // Find the CLI package directory (where node_modules lives).\n // This file is at packages/cli/dist/cli.js (or src/commands/studio.ts in dev).\n const cliPackageDir = resolve(dirname(new URL(import.meta.url).pathname), \"..\");\n\n // Create temp directory with inline app.\n // Use realpathSync to resolve macOS /var -> /private/var symlink,\n // which Vite normalizes internally when resolving file paths.\n const tmpId = randomBytes(4).toString(\"hex\");\n const tmpDirRaw = join(tmpdir(), `atelier-studio-${tmpId}`);\n mkdirSync(tmpDirRaw, { recursive: true });\n const tmpDir = realpathSync(tmpDirRaw);\n\n writeFileSync(join(tmpDir, \"index.html\"), getInlineHTML());\n writeFileSync(join(tmpDir, \"main.ts\"), getInlineApp(file ?? null));\n\n // Symlink node_modules into temp dir so Vite can resolve @a-company/* packages.\n // Works for both monorepo (pnpm workspace links) and npm global install.\n const cliNodeModules = join(cliPackageDir, \"node_modules\");\n if (existsSync(cliNodeModules)) {\n try {\n symlinkSync(cliNodeModules, join(tmpDir, \"node_modules\"), \"dir\");\n } catch {\n // Non-fatal: aliases will handle resolution if symlink fails\n }\n }\n\n console.log(`Starting Atelier Studio...`);\n console.log(` Working directory: ${cwd}`);\n\n // Dynamically import Vite (it's a peer/optional dep)\n let vite: typeof import(\"vite\");\n try {\n vite = await import(\"vite\");\n } catch {\n console.error(\"Vite is required for `atelier studio`.\");\n console.error(\"Install it: pnpm add -D vite\");\n process.exit(1);\n return;\n }\n\n const server = await vite.createServer({\n root: tmpDir,\n server: {\n port,\n strictPort: false,\n fs: {\n strict: false,\n },\n },\n plugins: [\n {\n name: \"atelier-api\",\n configureServer(server) {\n server.middlewares.use((req, res, next) => {\n const url = new URL(req.url ?? \"/\", `http://localhost:${port}`);\n\n if (url.pathname === \"/api/files\") {\n const atelierFiles = findAtelierFiles(cwd);\n const entries = atelierFiles.map((p) => {\n const parts = p.split(\"/\");\n return {\n path: p,\n name: parts[parts.length - 1].replace(\".atelier\", \"\"),\n folder: parts.length > 1 ? parts.slice(0, -1).join(\"/\") : \"\",\n };\n });\n res.setHeader(\"Content-Type\", \"application/json\");\n res.end(JSON.stringify(entries));\n return;\n }\n\n if (url.pathname === \"/api/file\") {\n const filePath = url.searchParams.get(\"path\");\n if (!filePath || !isSafePath(filePath)) {\n res.statusCode = 400;\n res.end(\"Invalid path\");\n return;\n }\n\n const absPath = resolve(cwd, filePath);\n\n if (req.method === \"GET\") {\n try {\n const content = readFileSync(absPath, \"utf-8\");\n res.setHeader(\"Content-Type\", \"text/plain\");\n res.end(content);\n } catch {\n res.statusCode = 404;\n res.end(\"File not found\");\n }\n return;\n }\n\n if (req.method === \"POST\") {\n let body = \"\";\n req.on(\"data\", (chunk: Buffer) => { body += chunk.toString(); });\n req.on(\"end\", () => {\n try {\n writeFileSync(absPath, body, \"utf-8\");\n res.end(\"OK\");\n } catch {\n res.statusCode = 500;\n res.end(\"Write failed\");\n }\n });\n return;\n }\n }\n\n if (url.pathname === \"/api/export\" && req.method === \"POST\") {\n const filePath = url.searchParams.get(\"path\");\n if (!filePath || !isSafePath(filePath)) {\n res.statusCode = 400;\n res.end(\"Invalid path\");\n return;\n }\n\n const absPath = resolve(cwd, filePath);\n const chunks: Buffer[] = [];\n req.on(\"data\", (chunk: Buffer) => { chunks.push(chunk); });\n req.on(\"end\", () => {\n try {\n mkdirSync(dirname(absPath), { recursive: true });\n writeFileSync(absPath, Buffer.concat(chunks));\n res.end(\"OK\");\n } catch {\n res.statusCode = 500;\n res.end(\"Write failed\");\n }\n });\n return;\n }\n\n if (url.pathname === \"/api/cwd\") {\n res.setHeader(\"Content-Type\", \"application/json\");\n res.end(JSON.stringify({ cwd }));\n return;\n }\n\n next();\n });\n },\n },\n ],\n logLevel: \"warn\",\n });\n\n await server.listen();\n\n const resolvedUrl = server.resolvedUrls?.local[0] ?? `http://localhost:${port}`;\n const url = resolvedUrl;\n\n console.log(` Server running at: ${url}`);\n\n const atelierFiles = findAtelierFiles(cwd);\n console.log(` Found ${atelierFiles.length} .atelier file(s)`);\n\n if (file) {\n console.log(` Opening: ${file}`);\n }\n\n console.log(` Press Ctrl+C to stop\\n`);\n\n if (options.open) {\n exec(`open \"${url}\"`);\n }\n\n // Keep alive and handle cleanup\n const cleanup = () => {\n console.log(\"\\nShutting down...\");\n server.close();\n try {\n rmSync(tmpDir, { recursive: true, force: true });\n } catch {\n // ignore cleanup errors\n }\n process.exit(0);\n };\n\n process.on(\"SIGINT\", cleanup);\n process.on(\"SIGTERM\", cleanup);\n },\n );\n}\n"],"mappings":";;;;;;;;;;;;;;AAGA,SAAS,qBAAqB;AAC9B,SAAS,eAAe;;;ACUxB,SAAS,SAAS,MAAM,UAAU,eAAe;AACjD,SAAS,WAAW,eAAe,QAAQ,cAAc,aAAa,UAAU,cAAc,aAAa,kBAAkB;AAC7H,SAAS,cAAc;AACvB,SAAS,mBAAmB;AAC5B,SAAS,YAAY;AAIrB,SAAS,iBAAiB,KAAa,OAAe,KAAe;AACnE,QAAM,UAAoB,CAAC;AAC3B,MAAI;AACJ,MAAI;AACF,cAAU,YAAY,GAAG;AAAA,EAC3B,QAAQ;AACN,WAAO;AAAA,EACT;AACA,aAAW,SAAS,SAAS;AAC3B,QAAI,UAAU,kBAAkB,UAAU,UAAU,UAAU,OAAQ;AACtE,UAAM,OAAO,KAAK,KAAK,KAAK;AAC5B,QAAI;AACJ,QAAI;AACF,aAAO,SAAS,IAAI;AAAA,IACtB,QAAQ;AACN;AAAA,IACF;AACA,QAAI,KAAK,YAAY,GAAG;AACtB,cAAQ,KAAK,GAAG,iBAAiB,MAAM,IAAI,CAAC;AAAA,IAC9C,WAAW,MAAM,SAAS,UAAU,GAAG;AACrC,cAAQ,KAAK,SAAS,MAAM,IAAI,CAAC;AAAA,IACnC;AAAA,EACF;AACA,SAAO,QAAQ,KAAK;AACtB;AAGA,SAAS,WAAW,UAA2B;AAC7C,MAAI,CAAC,YAAY,SAAS,SAAS,IAAI,KAAK,SAAS,WAAW,GAAG,EAAG,QAAO;AAC7E,QAAM,WAAW,QAAQ,QAAQ,IAAI,GAAG,QAAQ;AAChD,SAAO,SAAS,WAAW,QAAQ,IAAI,CAAC;AAC1C;AAEA,SAAS,gBAAwB;AAC/B,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAeT;AAEA,SAAS,aAAa,aAAoC;AACxD,QAAM,iBAAiB,cAAc,KAAK,UAAU,WAAW,IAAI;AACnE,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA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","names":["program","server","url","atelierFiles"]}
1
+ {"version":3,"sources":["../src/cli.ts","../src/commands/lint.ts","../src/commands/studio.ts"],"sourcesContent":["#!/usr/bin/env node\n// @a-company/atelier-cli — Entry point for the `atelier` command\n\nimport { createRequire } from \"node:module\";\nimport { Command } from \"commander\";\nimport { validateCommand } from \"./commands/validate.js\";\nimport { lintCommand } from \"./commands/lint.js\";\nimport { infoCommand } from \"./commands/info.js\";\nimport { stillCommand } from \"./commands/still.js\";\nimport { renderCommand } from \"./commands/render.js\";\nimport { exportSvgCommand } from \"./commands/export-svg.js\";\nimport { exportLottieCommand } from \"./commands/export-lottie.js\";\nimport { assetsCommand } from \"./commands/assets.js\";\nimport { variablesCommand } from \"./commands/variables.js\";\nimport { studioCommand } from \"./commands/studio.js\";\n\nconst program = new Command();\n\nprogram\n .name(\"atelier\")\n .description(\"Atelier animation CLI\")\n .version(createRequire(import.meta.url)(\"../package.json\").version);\n\n// Register commands\nvalidateCommand(program);\nlintCommand(program);\ninfoCommand(program);\nstillCommand(program);\nrenderCommand(program);\nexportSvgCommand(program);\nexportLottieCommand(program);\nassetsCommand(program);\nvariablesCommand(program);\nstudioCommand(program);\n\nprogram.parse();\n","import { readFileSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport type { Command } from \"commander\";\nimport type { VideoVisual } from \"@a-company/atelier-types\";\nimport { parseAtelier, validateVideoLayer } from \"@a-company/atelier-schema\";\nimport { validateAllDeltas } from \"@a-company/atelier-core\";\n\nexport interface GateResult {\n gate: string;\n pass: boolean;\n errors: string[];\n}\n\nexport interface LintResult {\n file: string;\n valid: boolean;\n gates: GateResult[];\n}\n\n/**\n * Run all gates against a single .atelier file.\n * Gates checked:\n * ^valid-document — Zod schema conformance\n * ^valid-delta — no overlapping deltas on same layer+property\n * ^valid-video-layer — semantic video layer constraints\n */\nexport function lintFile(filePath: string): LintResult {\n const absPath = resolve(filePath);\n\n let content: string;\n try {\n content = readFileSync(absPath, \"utf-8\");\n } catch {\n return {\n file: absPath,\n valid: false,\n gates: [\n {\n gate: \"^valid-document\",\n pass: false,\n errors: [`Cannot read file: ${absPath}`],\n },\n ],\n };\n }\n\n const gates: GateResult[] = [];\n\n // ── Gate 1: ^valid-document ──────────────────────────────────\n const parseResult = parseAtelier(content);\n if (!parseResult.success) {\n gates.push({\n gate: \"^valid-document\",\n pass: false,\n errors: parseResult.errors.map((e) => `${e.path}: ${e.message}`),\n });\n // Can't run further gates without a valid document\n return { file: absPath, valid: false, gates };\n }\n\n gates.push({ gate: \"^valid-document\", pass: true, errors: [] });\n const doc = parseResult.data;\n\n // ── Gate 2: ^valid-delta ─────────────────────────────────────\n const deltaErrors: string[] = [];\n for (const [stateName, state] of Object.entries(doc.states)) {\n const overlaps = validateAllDeltas(state.deltas);\n for (const overlap of overlaps) {\n deltaErrors.push(`State \"${stateName}\": ${overlap.message}`);\n }\n }\n gates.push({\n gate: \"^valid-delta\",\n pass: deltaErrors.length === 0,\n errors: deltaErrors,\n });\n\n // ── Gate 3: ^valid-video-layer ───────────────────────────────\n const videoErrors: string[] = [];\n for (const layer of doc.layers) {\n if (layer.visual.type !== \"video\") continue;\n const visual = layer.visual as VideoVisual;\n const duration = doc.assets?.[visual.assetId]?.videoMeta?.duration;\n const result = validateVideoLayer(visual, duration);\n if (!result.success) {\n for (const err of result.errors) {\n videoErrors.push(`Layer \"${layer.id}\" (${err.path}): ${err.message}`);\n }\n }\n }\n gates.push({\n gate: \"^valid-video-layer\",\n pass: videoErrors.length === 0,\n errors: videoErrors,\n });\n\n const valid = gates.every((g) => g.pass);\n return { file: absPath, valid, gates };\n}\n\n/** Format a LintResult for terminal output */\nfunction formatResult(result: LintResult): string {\n const lines: string[] = [];\n const status = result.valid ? \"PASS\" : \"FAIL\";\n lines.push(`${status} ${result.file}`);\n\n for (const gate of result.gates) {\n const gateStatus = gate.pass ? \" ✓\" : \" ✗\";\n lines.push(`${gateStatus} ${gate.gate}`);\n for (const err of gate.errors) {\n lines.push(` ${err}`);\n }\n }\n\n return lines.join(\"\\n\");\n}\n\n/**\n * Register the `lint` subcommand on the Commander program.\n */\nexport function lintCommand(program: Command): void {\n program\n .command(\"lint <files...>\")\n .description(\n \"Lint .atelier files against all gates (^valid-document, ^valid-delta, ^valid-video-layer)\",\n )\n .option(\"--json\", \"Output results as JSON array\")\n .action((files: string[], opts: { json?: boolean }) => {\n const results = files.map(lintFile);\n\n if (opts.json) {\n console.log(JSON.stringify(results, null, 2));\n } else {\n for (const result of results) {\n console.log(formatResult(result));\n }\n }\n\n const allValid = results.every((r) => r.valid);\n if (!allValid) process.exit(1);\n });\n}\n","/**\n * `atelier studio [file]` — launch the browser-based Atelier editor.\n *\n * Spins up a Vite dev server with a temporary app that imports AtelierStudio,\n * provides a file API for reading/writing .atelier files from CWD, and opens\n * the browser.\n *\n * Usage:\n * atelier studio → browse all .atelier files in CWD\n * atelier studio my-animation.atelier → open specific file\n * atelier studio --port 8080 → custom port\n * atelier studio --no-open → don't auto-open browser\n */\n\nimport { resolve, join, relative, dirname } from \"node:path\";\nimport { mkdirSync, writeFileSync, rmSync, readFileSync, readdirSync, statSync, realpathSync, symlinkSync, existsSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { randomBytes } from \"node:crypto\";\nimport { exec } from \"node:child_process\";\nimport type { Command } from \"commander\";\n\n/** Recursively glob for .atelier files under a directory. */\nfunction findAtelierFiles(dir: string, base: string = dir): string[] {\n const results: string[] = [];\n let entries: string[];\n try {\n entries = readdirSync(dir);\n } catch {\n return results;\n }\n for (const entry of entries) {\n if (entry === \"node_modules\" || entry === \"dist\" || entry === \".git\") continue;\n const full = join(dir, entry);\n let stat;\n try {\n stat = statSync(full);\n } catch {\n continue;\n }\n if (stat.isDirectory()) {\n results.push(...findAtelierFiles(full, base));\n } else if (entry.endsWith(\".atelier\")) {\n results.push(relative(base, full));\n }\n }\n return results.sort();\n}\n\n/** Validate that a file path is safe (relative, no traversal). */\nfunction isSafePath(filePath: string): boolean {\n if (!filePath || filePath.includes(\"..\") || filePath.startsWith(\"/\")) return false;\n const resolved = resolve(process.cwd(), filePath);\n return resolved.startsWith(process.cwd());\n}\n\nfunction getInlineHTML(): string {\n return `<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <title>Atelier Studio</title>\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link href=\"https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400&display=swap\" rel=\"stylesheet\">\n </head>\n <body>\n <div id=\"studio\"></div>\n <script type=\"module\" src=\"/main.ts\"></script>\n </body>\n</html>`;\n}\n\nfunction getInlineApp(initialFile: string | null): string {\n const initialFileStr = initialFile ? JSON.stringify(initialFile) : \"null\";\n return `import { AtelierStudio, exportDocument, ImageCache } from \"@a-company/atelier-studio\";\nimport \"@a-company/atelier-studio/styles.css\";\nimport { parseAtelier, serializeAtelier } from \"@a-company/atelier-schema\";\n\n// ── Types ──\ninterface FileEntry {\n path: string;\n name: string;\n folder: string;\n}\n\n// ── State ──\nlet studio: AtelierStudio | null = null;\nlet currentFile: string | null = null;\nlet files: FileEntry[] = [];\nlet saveTimeout: ReturnType<typeof setTimeout> | null = null;\n\n// ── API helpers ──\nasync function fetchFiles(): Promise<FileEntry[]> {\n const res = await fetch(\"/api/files\");\n return res.json();\n}\n\nasync function fetchFileContent(path: string): Promise<string> {\n const res = await fetch(\"/api/file?path=\" + encodeURIComponent(path));\n return res.text();\n}\n\nasync function saveFileContent(path: string, content: string): Promise<void> {\n await fetch(\"/api/file?path=\" + encodeURIComponent(path), {\n method: \"POST\",\n headers: { \"Content-Type\": \"text/plain\" },\n body: content,\n });\n}\n\nasync function saveExportBlob(path: string, blob: Blob): Promise<void> {\n const buf = await blob.arrayBuffer();\n await fetch(\"/api/export?path=\" + encodeURIComponent(path), {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/octet-stream\" },\n body: buf,\n });\n}\n\nasync function exportAll(format: \"gif\" | \"mp4\" | \"webm\"): Promise<void> {\n if (files.length === 0) return;\n\n // Create progress overlay\n const overlay = document.createElement(\"div\");\n overlay.style.cssText = \"position:fixed;inset:0;background:rgba(0,0,0,0.75);display:flex;align-items:center;justify-content:center;z-index:10000\";\n const card = document.createElement(\"div\");\n card.style.cssText = \"background:#333;border:1px solid #4A4A4A;border-radius:8px;padding:32px 40px;min-width:360px;color:#F5F0EB;font-family:'Cormorant Garamond',Georgia,serif\";\n overlay.appendChild(card);\n document.body.appendChild(overlay);\n\n const title = document.createElement(\"div\");\n title.style.cssText = \"font-size:18px;margin-bottom:16px;font-weight:600\";\n title.textContent = \"Exporting All Files…\";\n card.appendChild(title);\n\n const fileLabel = document.createElement(\"div\");\n fileLabel.style.cssText = \"font-size:13px;color:#A89F95;margin-bottom:8px;font-family:'SF Mono','Fira Code',monospace\";\n card.appendChild(fileLabel);\n\n const progress = document.createElement(\"progress\");\n progress.style.cssText = \"width:100%;height:6px;appearance:none;-webkit-appearance:none\";\n progress.max = files.length;\n progress.value = 0;\n card.appendChild(progress);\n\n const statusText = document.createElement(\"div\");\n statusText.style.cssText = \"font-size:12px;color:#A89F95;margin-top:8px\";\n card.appendChild(statusText);\n\n let exported = 0;\n let errors = 0;\n\n for (const file of files) {\n fileLabel.textContent = file.path;\n statusText.textContent = (exported + errors + 1) + \" / \" + files.length;\n\n try {\n const content = await fetchFileContent(file.path);\n const result = parseAtelier(content);\n if (!result.success) {\n errors++;\n progress.value = exported + errors;\n continue;\n }\n\n const doc = result.data;\n const w = doc.canvas.width;\n const h = doc.canvas.height;\n const canvas = document.createElement(\"canvas\");\n canvas.width = w;\n canvas.height = h;\n const imageCache = new ImageCache();\n\n const exportResult = await exportDocument(doc, canvas, imageCache, {\n format,\n onProgress: ({ percent }) => {\n statusText.textContent = (exported + errors + 1) + \" / \" + files.length + \" — \" + percent + \"%\";\n },\n });\n\n // Save alongside the source file: e.g. \"dir/my-anim.atelier\" → \"dir/my-anim.gif\"\n const outPath = file.path.replace(/\\\\.atelier$/, \".\" + format);\n await saveExportBlob(outPath, exportResult.blob);\n exported++;\n } catch (e) {\n console.error(\"Export failed:\", file.path, e);\n errors++;\n }\n progress.value = exported + errors;\n }\n\n // Done\n title.textContent = \"Export Complete\";\n fileLabel.textContent = \"\";\n statusText.textContent = exported + \" exported\" + (errors > 0 ? \", \" + errors + \" failed\" : \"\");\n if (errors > 0) console.warn(\"Export All finished with \" + errors + \" error(s). Check console for details.\");\n\n const closeBtn = document.createElement(\"button\");\n closeBtn.style.cssText = \"margin-top:16px;padding:6px 20px;background:#3D3D3D;color:#F5F0EB;border:1px solid #4A4A4A;border-radius:4px;cursor:pointer;font-family:inherit;font-size:13px\";\n closeBtn.textContent = \"Close\";\n closeBtn.addEventListener(\"click\", () => document.body.removeChild(overlay));\n card.appendChild(closeBtn);\n}\n\n// ── Theme (matches branded theme from showcase) ──\nconst theme = {\n bg: \"#2C2C2C\",\n bgSecondary: \"#333333\",\n bgTertiary: \"#3D3D3D\",\n text: \"#F5F0EB\",\n textMuted: \"#A89F95\",\n textAccent: \"#F5F0EB\",\n border: \"#4A4A4A\",\n buttonBg: \"#3D3D3D\",\n buttonHover: \"#4A4A4A\",\n buttonActive: \"#555555\",\n accent: \"#C75B39\",\n accentHover: \"#D4724E\",\n sliderTrack: \"#4A4A4A\",\n sliderThumb: \"#C75B39\",\n fontFamily: \"'Cormorant Garamond', Georgia, serif\",\n fontMono: \"'SF Mono', 'Fira Code', monospace\",\n canvasShadow: \"0 4px 60px rgba(199, 91, 57, 0.12), 0 0 40px rgba(0,0,0,0.4)\",\n};\n\n// ── Styles ──\nconst style = document.createElement(\"style\");\nstyle.textContent = \\`\n * { margin: 0; padding: 0; box-sizing: border-box; }\n html, body { height: 100%; overflow: hidden; background: #2C2C2C; color: #F5F0EB; }\n body { font-family: 'Cormorant Garamond', Georgia, serif; }\n #studio { display: flex; height: 100vh; width: 100vw; }\n\n .sidebar {\n width: 260px;\n min-width: 260px;\n background: #333333;\n border-right: 1px solid #4A4A4A;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n }\n .sidebar__header {\n padding: 16px 20px;\n border-bottom: 1px solid #4A4A4A;\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 2px;\n text-transform: uppercase;\n color: #A89F95;\n display: flex;\n align-items: center;\n gap: 8px;\n }\n .sidebar__header span {\n color: #C75B39;\n font-size: 13px;\n }\n .sidebar__list {\n flex: 1;\n overflow-y: auto;\n padding: 8px 0;\n }\n .sidebar__list::-webkit-scrollbar { width: 6px; }\n .sidebar__list::-webkit-scrollbar-track { background: transparent; }\n .sidebar__list::-webkit-scrollbar-thumb { background: #4A4A4A; border-radius: 3px; }\n\n .sidebar__folder {\n padding: 10px 20px 4px;\n font-size: 10px;\n font-weight: 600;\n letter-spacing: 1.5px;\n text-transform: uppercase;\n color: #A89F95;\n }\n .sidebar__item {\n padding: 8px 20px 8px 28px;\n font-size: 13px;\n cursor: pointer;\n color: #A89F95;\n transition: background 0.15s, color 0.15s;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n font-family: 'SF Mono', 'Fira Code', monospace;\n font-size: 11.5px;\n }\n .sidebar__item:hover { background: #363636; color: #F5F0EB; }\n .sidebar__item--active {\n background: rgba(199, 91, 57, 0.12) !important;\n color: #C75B39 !important;\n }\n\n .main {\n flex: 1;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n }\n .main__status {\n height: 32px;\n min-height: 32px;\n display: flex;\n align-items: center;\n padding: 0 16px;\n background: #333333;\n border-bottom: 1px solid #4A4A4A;\n font-size: 11px;\n color: #A89F95;\n font-family: 'SF Mono', 'Fira Code', monospace;\n gap: 12px;\n }\n .main__status .save-indicator {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n margin-left: auto;\n transition: opacity 0.3s;\n }\n .main__status .save-indicator--saving { color: #C75B39; }\n .main__status .save-indicator--saved { color: #6B8E6B; }\n .main__editor {\n flex: 1;\n overflow: hidden;\n }\n .main__empty {\n flex: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n color: #A89F95;\n font-size: 18px;\n }\n\\`;\ndocument.head.appendChild(style);\n\n// ── Build UI ──\nconst root = document.getElementById(\"studio\")!;\nconst sidebar = document.createElement(\"div\");\nsidebar.className = \"sidebar\";\n\nconst sidebarHeader = document.createElement(\"div\");\nsidebarHeader.className = \"sidebar__header\";\nsidebarHeader.innerHTML = '<span>&#9670;</span> ATELIER STUDIO';\nsidebar.appendChild(sidebarHeader);\n\nconst sidebarList = document.createElement(\"div\");\nsidebarList.className = \"sidebar__list\";\nsidebar.appendChild(sidebarList);\n\nconst sidebarFooter = document.createElement(\"div\");\nsidebarFooter.style.cssText = \"padding:12px 16px;border-top:1px solid #4A4A4A;display:flex;gap:8px;align-items:center\";\nconst exportAllSelect = document.createElement(\"select\");\nexportAllSelect.style.cssText = \"flex:1;background:#3D3D3D;color:#F5F0EB;border:1px solid #4A4A4A;border-radius:4px;padding:4px 8px;font-size:11px;font-family:'SF Mono','Fira Code',monospace;cursor:pointer\";\nfor (const [val, label] of [[\"gif\",\"GIF\"],[\"mp4\",\"MP4\"],[\"webm\",\"WebM\"]] as const) {\n const o = document.createElement(\"option\");\n o.value = val;\n o.textContent = label;\n exportAllSelect.appendChild(o);\n}\nsidebarFooter.appendChild(exportAllSelect);\nconst exportAllBtn = document.createElement(\"button\");\nexportAllBtn.style.cssText = \"background:#C75B39;color:#F5F0EB;border:none;border-radius:4px;padding:5px 12px;font-size:11px;font-family:inherit;cursor:pointer;white-space:nowrap\";\nexportAllBtn.textContent = \"Export All\";\nexportAllBtn.addEventListener(\"click\", () => {\n exportAll(exportAllSelect.value as \"gif\" | \"mp4\" | \"webm\");\n});\nsidebarFooter.appendChild(exportAllBtn);\nsidebar.appendChild(sidebarFooter);\n\nconst main = document.createElement(\"div\");\nmain.className = \"main\";\n\nconst statusBar = document.createElement(\"div\");\nstatusBar.className = \"main__status\";\nmain.appendChild(statusBar);\n\nconst editorContainer = document.createElement(\"div\");\neditorContainer.className = \"main__editor\";\nmain.appendChild(editorContainer);\n\nroot.appendChild(sidebar);\nroot.appendChild(main);\n\n// ── File list rendering ──\nfunction renderFileList(): void {\n sidebarList.innerHTML = \"\";\n let lastFolder = \"\";\n\n for (const file of files) {\n if (file.folder && file.folder !== lastFolder) {\n lastFolder = file.folder;\n const folder = document.createElement(\"div\");\n folder.className = \"sidebar__folder\";\n folder.textContent = file.folder;\n sidebarList.appendChild(folder);\n }\n\n const item = document.createElement(\"div\");\n item.className = \"sidebar__item\" + (file.path === currentFile ? \" sidebar__item--active\" : \"\");\n item.textContent = file.name;\n item.title = file.path;\n item.addEventListener(\"click\", () => loadFile(file.path));\n sidebarList.appendChild(item);\n }\n}\n\n// ── Load a file into the studio ──\nasync function loadFile(path: string): Promise<void> {\n currentFile = path;\n renderFileList();\n\n const content = await fetchFileContent(path);\n const result = parseAtelier(content);\n\n if (!result.success) {\n editorContainer.innerHTML = \"\";\n const err = document.createElement(\"div\");\n err.className = \"main__empty\";\n err.style.flexDirection = \"column\";\n err.style.gap = \"8px\";\n err.innerHTML = '<div style=\"color:#C75B39\">Parse Error</div><div style=\"font-size:13px;font-family:monospace\">' +\n result.errors.map(e => e.path + \": \" + e.message).join(\"<br>\") + \"</div>\";\n editorContainer.appendChild(err);\n return;\n }\n\n statusBar.innerHTML = '<span>' + path + '</span><span class=\"save-indicator save-indicator--saved\">&#10003; saved</span>';\n\n if (studio) {\n studio.destroy();\n studio = null;\n }\n\n // Set filename for export downloads (strip path and .atelier extension)\n const baseName = path.split(\"/\").pop()?.replace(/\\\\.atelier$/, \"\") || null;\n\n studio = new AtelierStudio(editorContainer, {\n mode: \"full\",\n initialTab: \"yaml\",\n allowSave: true,\n onDocumentChange: (doc) => {\n // Auto-save with debounce\n const indicator = statusBar.querySelector(\".save-indicator\");\n if (indicator) {\n indicator.className = \"save-indicator save-indicator--saving\";\n indicator.innerHTML = \"&#9679; saving...\";\n }\n\n if (saveTimeout) clearTimeout(saveTimeout);\n saveTimeout = setTimeout(async () => {\n if (!currentFile) return;\n const yaml = serializeAtelier(doc);\n await saveFileContent(currentFile, yaml);\n const ind = statusBar.querySelector(\".save-indicator\");\n if (ind) {\n ind.className = \"save-indicator save-indicator--saved\";\n ind.innerHTML = \"&#10003; saved\";\n }\n }, 800);\n },\n });\n studio.setTheme(theme);\n studio.setFilename(baseName);\n studio.loadDocument(result.data);\n}\n\n// ── Boot ──\nasync function boot(): Promise<void> {\n files = await fetchFiles();\n\n if (files.length === 0) {\n editorContainer.innerHTML = \"\";\n const empty = document.createElement(\"div\");\n empty.className = \"main__empty\";\n empty.textContent = \"No .atelier files found in this directory\";\n editorContainer.appendChild(empty);\n statusBar.textContent = \"No files\";\n renderFileList();\n return;\n }\n\n renderFileList();\n\n const initialFile = ${initialFileStr};\n const target = initialFile\n ? files.find(f => f.path === initialFile || f.path.endsWith(initialFile))\n : files[0];\n\n if (target) {\n await loadFile(target.path);\n }\n}\n\nboot();\n`;\n}\n\n/** Register the `studio` subcommand on the Commander program. */\nexport function studioCommand(program: Command): void {\n program\n .command(\"studio [file]\")\n .description(\"Launch the browser-based Atelier editor\")\n .option(\"-p, --port <number>\", \"Port to serve on\", \"4321\")\n .option(\"--no-open\", \"Don't auto-open browser\")\n .action(\n async (\n file: string | undefined,\n options: { port: string; open: boolean },\n ) => {\n const port = parseInt(options.port, 10);\n if (isNaN(port) || port < 1 || port > 65535) {\n console.error(`Invalid port: ${options.port}`);\n process.exit(1);\n }\n\n const cwd = process.cwd();\n\n // Find the CLI package directory (where node_modules lives).\n // This file is at packages/cli/dist/cli.js (or src/commands/studio.ts in dev).\n const cliPackageDir = resolve(dirname(new URL(import.meta.url).pathname), \"..\");\n\n // Create temp directory with inline app.\n // Use realpathSync to resolve macOS /var -> /private/var symlink,\n // which Vite normalizes internally when resolving file paths.\n const tmpId = randomBytes(4).toString(\"hex\");\n const tmpDirRaw = join(tmpdir(), `atelier-studio-${tmpId}`);\n mkdirSync(tmpDirRaw, { recursive: true });\n const tmpDir = realpathSync(tmpDirRaw);\n\n writeFileSync(join(tmpDir, \"index.html\"), getInlineHTML());\n writeFileSync(join(tmpDir, \"main.ts\"), getInlineApp(file ?? null));\n\n // Symlink node_modules into temp dir so Vite can resolve @a-company/* packages.\n // Works for both monorepo (pnpm workspace links) and npm global install.\n const cliNodeModules = join(cliPackageDir, \"node_modules\");\n if (existsSync(cliNodeModules)) {\n try {\n symlinkSync(cliNodeModules, join(tmpDir, \"node_modules\"), \"dir\");\n } catch {\n // Non-fatal: aliases will handle resolution if symlink fails\n }\n }\n\n console.log(`Starting Atelier Studio...`);\n console.log(` Working directory: ${cwd}`);\n\n // Dynamically import Vite (it's a peer/optional dep)\n let vite: typeof import(\"vite\");\n try {\n vite = await import(\"vite\");\n } catch {\n console.error(\"Vite is required for `atelier studio`.\");\n console.error(\"Install it: pnpm add -D vite\");\n process.exit(1);\n return;\n }\n\n const server = await vite.createServer({\n root: tmpDir,\n server: {\n port,\n strictPort: false,\n fs: {\n strict: false,\n },\n },\n plugins: [\n {\n name: \"atelier-api\",\n configureServer(server) {\n server.middlewares.use((req, res, next) => {\n const url = new URL(req.url ?? \"/\", `http://localhost:${port}`);\n\n if (url.pathname === \"/api/files\") {\n const atelierFiles = findAtelierFiles(cwd);\n const entries = atelierFiles.map((p) => {\n const parts = p.split(\"/\");\n return {\n path: p,\n name: parts[parts.length - 1].replace(\".atelier\", \"\"),\n folder: parts.length > 1 ? parts.slice(0, -1).join(\"/\") : \"\",\n };\n });\n res.setHeader(\"Content-Type\", \"application/json\");\n res.end(JSON.stringify(entries));\n return;\n }\n\n if (url.pathname === \"/api/file\") {\n const filePath = url.searchParams.get(\"path\");\n if (!filePath || !isSafePath(filePath)) {\n res.statusCode = 400;\n res.end(\"Invalid path\");\n return;\n }\n\n const absPath = resolve(cwd, filePath);\n\n if (req.method === \"GET\") {\n try {\n const content = readFileSync(absPath, \"utf-8\");\n res.setHeader(\"Content-Type\", \"text/plain\");\n res.end(content);\n } catch {\n res.statusCode = 404;\n res.end(\"File not found\");\n }\n return;\n }\n\n if (req.method === \"POST\") {\n let body = \"\";\n req.on(\"data\", (chunk: Buffer) => { body += chunk.toString(); });\n req.on(\"end\", () => {\n try {\n writeFileSync(absPath, body, \"utf-8\");\n res.end(\"OK\");\n } catch {\n res.statusCode = 500;\n res.end(\"Write failed\");\n }\n });\n return;\n }\n }\n\n if (url.pathname === \"/api/export\" && req.method === \"POST\") {\n const filePath = url.searchParams.get(\"path\");\n if (!filePath || !isSafePath(filePath)) {\n res.statusCode = 400;\n res.end(\"Invalid path\");\n return;\n }\n\n const absPath = resolve(cwd, filePath);\n const chunks: Buffer[] = [];\n req.on(\"data\", (chunk: Buffer) => { chunks.push(chunk); });\n req.on(\"end\", () => {\n try {\n mkdirSync(dirname(absPath), { recursive: true });\n writeFileSync(absPath, Buffer.concat(chunks));\n res.end(\"OK\");\n } catch {\n res.statusCode = 500;\n res.end(\"Write failed\");\n }\n });\n return;\n }\n\n if (url.pathname === \"/api/cwd\") {\n res.setHeader(\"Content-Type\", \"application/json\");\n res.end(JSON.stringify({ cwd }));\n return;\n }\n\n next();\n });\n },\n },\n ],\n logLevel: \"warn\",\n });\n\n await server.listen();\n\n const resolvedUrl = server.resolvedUrls?.local[0] ?? `http://localhost:${port}`;\n const url = resolvedUrl;\n\n console.log(` Server running at: ${url}`);\n\n const atelierFiles = findAtelierFiles(cwd);\n console.log(` Found ${atelierFiles.length} .atelier file(s)`);\n\n if (file) {\n console.log(` Opening: ${file}`);\n }\n\n console.log(` Press Ctrl+C to stop\\n`);\n\n if (options.open) {\n exec(`open \"${url}\"`);\n }\n\n // Keep alive and handle cleanup\n const cleanup = () => {\n console.log(\"\\nShutting down...\");\n server.close();\n try {\n rmSync(tmpDir, { recursive: true, force: true });\n } catch {\n // ignore cleanup errors\n }\n process.exit(0);\n };\n\n process.on(\"SIGINT\", cleanup);\n process.on(\"SIGTERM\", cleanup);\n },\n );\n}\n"],"mappings":"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AAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;A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","names":["program","resolve","readFileSync","program","server","url","atelierFiles"]}
@@ -9,7 +9,7 @@ import {
9
9
  renderRef,
10
10
  renderShape,
11
11
  renderText
12
- } from "./chunk-C5DBTHXB.js";
12
+ } from "./chunk-JPZ4F4PW.js";
13
13
  export {
14
14
  ImageCache,
15
15
  applyFill,
@@ -22,4 +22,4 @@ export {
22
22
  renderShape,
23
23
  renderText
24
24
  };
25
- //# sourceMappingURL=dist-6IHF7WA7.js.map
25
+ //# sourceMappingURL=dist-M67UZGFQ.js.map
package/dist/index.cjs CHANGED
@@ -420,7 +420,18 @@ function resolveFrame(doc, stateName, frame, overrideDeltas) {
420
420
  }
421
421
  }
422
422
  }
423
- return { id: layer.id, layer, computedProperties };
423
+ const resolvedLayer = { id: layer.id, layer, computedProperties };
424
+ if (layer.visual.type === "video") {
425
+ const video = layer.visual;
426
+ const fps = doc.canvas.fps;
427
+ const startFrame = video.startFrame ?? 0;
428
+ const sourceOffset = video.sourceOffset ?? 0;
429
+ const playbackRate = video.playbackRate ?? 1;
430
+ const relativeFrame = Math.max(0, frame - startFrame);
431
+ const sourceTime = relativeFrame / fps * playbackRate + sourceOffset;
432
+ resolvedLayer.videoSourceTime = video.sourceEnd !== void 0 ? Math.min(sourceTime, video.sourceEnd) : sourceTime;
433
+ }
434
+ return resolvedLayer;
424
435
  });
425
436
  return { frame, stateName, layers: resolvedLayers };
426
437
  }
@@ -1027,6 +1038,14 @@ function renderImage(ctx, eff, imageCache) {
1027
1038
  }
1028
1039
  ctx.drawImage(img, 0, 0, eff.width, eff.height);
1029
1040
  }
1041
+ function renderVideo(ctx, eff, sourceTime, provider) {
1042
+ const visual = eff.visual;
1043
+ const src = visual.src;
1044
+ if (!src) return;
1045
+ const frame = provider(src, sourceTime, eff.width, eff.height);
1046
+ if (!frame) return;
1047
+ ctx.drawImage(frame, 0, 0, eff.width, eff.height);
1048
+ }
1030
1049
  function renderRef(ctx, eff, opts, _parentDoc) {
1031
1050
  const visual = eff.visual;
1032
1051
  const resolver = opts?.documentResolver;
@@ -1124,6 +1143,7 @@ function renderPlaceholder(ctx, eff, label) {
1124
1143
  function renderFrame(ctx, resolvedFrame, doc, optsOrCache) {
1125
1144
  let imageCache;
1126
1145
  let documentResolver;
1146
+ let videoFrameProvider;
1127
1147
  let maxRefDepth = 4;
1128
1148
  if (optsOrCache && typeof optsOrCache.get === "function") {
1129
1149
  imageCache = optsOrCache;
@@ -1131,6 +1151,7 @@ function renderFrame(ctx, resolvedFrame, doc, optsOrCache) {
1131
1151
  const opts = optsOrCache;
1132
1152
  imageCache = opts.imageCache;
1133
1153
  documentResolver = opts.documentResolver;
1154
+ videoFrameProvider = opts.videoFrameProvider;
1134
1155
  maxRefDepth = opts.maxRefDepth ?? 4;
1135
1156
  }
1136
1157
  const { width, height } = doc.canvas;
@@ -1138,10 +1159,14 @@ function renderFrame(ctx, resolvedFrame, doc, optsOrCache) {
1138
1159
  ctx.fillRect(0, 0, width, height);
1139
1160
  const effMap = /* @__PURE__ */ new Map();
1140
1161
  const effList = [];
1162
+ const videoSourceTimeMap = /* @__PURE__ */ new Map();
1141
1163
  for (const resolvedLayer of resolvedFrame.layers) {
1142
1164
  const eff = buildEffectiveLayer(resolvedLayer, width, height);
1143
1165
  effMap.set(resolvedLayer.layer.id, eff);
1144
1166
  effList.push(eff);
1167
+ if (resolvedLayer.videoSourceTime !== void 0) {
1168
+ videoSourceTimeMap.set(resolvedLayer.layer.id, resolvedLayer.videoSourceTime);
1169
+ }
1145
1170
  }
1146
1171
  for (const eff of effList) {
1147
1172
  const { layer } = eff;
@@ -1153,6 +1178,12 @@ function renderFrame(ctx, resolvedFrame, doc, optsOrCache) {
1153
1178
  iv.src = doc.assets[iv.assetId].src;
1154
1179
  }
1155
1180
  }
1181
+ if (layer.visual.type === "video") {
1182
+ const vv = eff.visual;
1183
+ if (!vv.src && vv.assetId && doc.assets?.[vv.assetId]) {
1184
+ vv.src = doc.assets[vv.assetId].src;
1185
+ }
1186
+ }
1156
1187
  ctx.save();
1157
1188
  applyAncestorTransforms(ctx, layer.id, effMap, doc);
1158
1189
  ctx.globalAlpha = eff.opacity;
@@ -1200,6 +1231,11 @@ function renderFrame(ctx, resolvedFrame, doc, optsOrCache) {
1200
1231
  case "image":
1201
1232
  if (imageCache) renderImage(renderCtx, eff, imageCache);
1202
1233
  break;
1234
+ case "video":
1235
+ if (videoFrameProvider) {
1236
+ renderVideo(renderCtx, eff, videoSourceTimeMap.get(layer.id) ?? 0, videoFrameProvider);
1237
+ }
1238
+ break;
1203
1239
  case "group":
1204
1240
  break;
1205
1241
  case "ref":
@@ -1225,6 +1261,9 @@ function renderFrame(ctx, resolvedFrame, doc, optsOrCache) {
1225
1261
  case "image":
1226
1262
  if (imageCache) renderImage(offCtx, eff, imageCache);
1227
1263
  break;
1264
+ case "video":
1265
+ if (videoFrameProvider) renderVideo(offCtx, eff, videoSourceTimeMap.get(layer.id) ?? 0, videoFrameProvider);
1266
+ break;
1228
1267
  case "ref":
1229
1268
  renderRef(offCtx, eff, refOpts, doc);
1230
1269
  break;
@@ -1240,6 +1279,9 @@ function renderFrame(ctx, resolvedFrame, doc, optsOrCache) {
1240
1279
  case "image":
1241
1280
  if (imageCache) renderImage(ctx, eff, imageCache);
1242
1281
  break;
1282
+ case "video":
1283
+ if (videoFrameProvider) renderVideo(ctx, eff, videoSourceTimeMap.get(layer.id) ?? 0, videoFrameProvider);
1284
+ break;
1243
1285
  case "ref":
1244
1286
  renderRef(ctx, eff, refOpts, doc);
1245
1287
  break;
@@ -1660,6 +1702,18 @@ var ImageVisualSchema = import_zod7.z.object({
1660
1702
  spritesheet: SpritesheetConfigSchema.optional(),
1661
1703
  frameIndex: import_zod7.z.number().int().min(0).optional()
1662
1704
  });
1705
+ var VideoVisualSchema = import_zod7.z.object({
1706
+ type: import_zod7.z.literal("video"),
1707
+ assetId: import_zod7.z.string().min(1, "assetId is required"),
1708
+ src: import_zod7.z.string().optional(),
1709
+ startFrame: import_zod7.z.number().int().min(0).optional(),
1710
+ sourceOffset: import_zod7.z.number().min(0).optional(),
1711
+ sourceEnd: import_zod7.z.number().positive().optional(),
1712
+ playbackRate: import_zod7.z.number().positive().optional(),
1713
+ volume: import_zod7.z.number().min(0).max(1).optional(),
1714
+ muted: import_zod7.z.boolean().optional(),
1715
+ objectFit: import_zod7.z.enum(["contain", "cover", "fill"]).optional()
1716
+ });
1663
1717
  var GroupVisualSchema = import_zod7.z.object({
1664
1718
  type: import_zod7.z.literal("group")
1665
1719
  });
@@ -1673,6 +1727,7 @@ var VisualSchema = import_zod7.z.discriminatedUnion("type", [
1673
1727
  ShapeVisualSchema,
1674
1728
  TextVisualSchema,
1675
1729
  ImageVisualSchema,
1730
+ VideoVisualSchema,
1676
1731
  GroupVisualSchema,
1677
1732
  RefVisualSchema
1678
1733
  ]);
@@ -1792,7 +1847,7 @@ var VariableSchema = import_zod12.z.object({
1792
1847
  default: import_zod12.z.unknown().optional(),
1793
1848
  description: import_zod12.z.string().optional()
1794
1849
  });
1795
- var AssetTypeSchema = import_zod13.z.enum(["image", "svg", "font", "animation", "audio"]);
1850
+ var AssetTypeSchema = import_zod13.z.enum(["image", "svg", "font", "animation", "audio", "video"]);
1796
1851
  var AssetSchema = import_zod13.z.object({
1797
1852
  type: AssetTypeSchema,
1798
1853
  src: import_zod13.z.string().min(1, "Asset src is required"),
@@ -1803,6 +1858,12 @@ var AssetSchema = import_zod13.z.object({
1803
1858
  frameCount: import_zod13.z.number().int().positive().optional(),
1804
1859
  frameWidth: import_zod13.z.number().positive(),
1805
1860
  frameHeight: import_zod13.z.number().positive()
1861
+ }).optional(),
1862
+ videoMeta: import_zod13.z.object({
1863
+ duration: import_zod13.z.number().positive("videoMeta.duration must be positive"),
1864
+ fps: import_zod13.z.number().positive("videoMeta.fps must be positive"),
1865
+ width: import_zod13.z.number().int().positive(),
1866
+ height: import_zod13.z.number().int().positive()
1806
1867
  }).optional()
1807
1868
  });
1808
1869
  var CanvasSchema = import_zod14.z.object({