domotion-svg 0.27.0 → 0.27.2

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.
Files changed (78) hide show
  1. package/assets/fonts/fixture/DomotionProfileDevanagariOne-Regular.ttf +0 -0
  2. package/assets/fonts/fixture/DomotionProfileDevanagariTwo-Regular.ttf +0 -0
  3. package/assets/fonts/fixture/README.md +16 -2
  4. package/dist/animation/embed-namespace.d.ts +2 -0
  5. package/dist/animation/embed-namespace.js +7 -3
  6. package/dist/animation/embed-timeline.d.ts +2 -0
  7. package/dist/animation/embed-timeline.js +9 -3
  8. package/dist/animation/svg-generator.d.ts +2 -0
  9. package/dist/animation/svg-generator.js +1 -0
  10. package/dist/cli/animate-orchestrator.js +15 -1
  11. package/dist/cli/common.js +1 -1
  12. package/dist/cli/composite.d.ts +131 -18
  13. package/dist/cli/composite.js +18 -16
  14. package/dist/cli/index.d.ts +1 -0
  15. package/dist/cli/index.js +8 -0
  16. package/dist/cli/storyboard.d.ts +891 -19
  17. package/dist/cli/storyboard.js +36 -21
  18. package/dist/cli/studio.d.ts +2 -0
  19. package/dist/cli/studio.js +68 -0
  20. package/dist/index.d.ts +2 -0
  21. package/dist/index.js +6 -0
  22. package/dist/scroll/composer.js +12 -5
  23. package/dist/scrubber/client.bundle.generated.js +1 -1
  24. package/dist/scrubber/client.d.ts +1 -0
  25. package/dist/scrubber/client.js +124 -19
  26. package/dist/scrubber/embed.d.ts +47 -0
  27. package/dist/scrubber/embed.js +71 -0
  28. package/dist/studio/agent-tools.d.ts +2833 -0
  29. package/dist/studio/agent-tools.js +495 -0
  30. package/dist/studio/annotations.d.ts +300 -0
  31. package/dist/studio/annotations.js +370 -0
  32. package/dist/studio/app-projects.d.ts +31 -0
  33. package/dist/studio/app-projects.js +137 -0
  34. package/dist/studio/authoring.d.ts +62 -0
  35. package/dist/studio/authoring.js +235 -0
  36. package/dist/studio/client.bundle.generated.d.ts +1 -0
  37. package/dist/studio/client.bundle.generated.js +3 -0
  38. package/dist/studio/client.d.ts +23 -0
  39. package/dist/studio/client.js +921 -0
  40. package/dist/studio/compile.d.ts +24 -0
  41. package/dist/studio/compile.js +165 -0
  42. package/dist/studio/cursor-choreography.d.ts +62 -0
  43. package/dist/studio/cursor-choreography.js +209 -0
  44. package/dist/studio/healing.d.ts +113 -0
  45. package/dist/studio/healing.js +279 -0
  46. package/dist/studio/index.d.ts +17 -0
  47. package/dist/studio/index.js +17 -0
  48. package/dist/studio/interaction-observer.d.ts +132 -0
  49. package/dist/studio/interaction-observer.js +417 -0
  50. package/dist/studio/interactions.d.ts +43 -0
  51. package/dist/studio/interactions.js +255 -0
  52. package/dist/studio/interactive-compile.d.ts +89 -0
  53. package/dist/studio/interactive-compile.js +399 -0
  54. package/dist/studio/project-json-schema.d.ts +3 -0
  55. package/dist/studio/project-json-schema.js +22 -0
  56. package/dist/studio/project-schema.d.ts +4225 -0
  57. package/dist/studio/project-schema.js +513 -0
  58. package/dist/studio/project.d.ts +32 -0
  59. package/dist/studio/project.js +183 -0
  60. package/dist/studio/recording.d.ts +3121 -0
  61. package/dist/studio/recording.js +579 -0
  62. package/dist/studio/server.d.ts +57 -0
  63. package/dist/studio/server.js +416 -0
  64. package/dist/studio/timeline.d.ts +67 -0
  65. package/dist/studio/timeline.js +413 -0
  66. package/dist/studio/treatment-schema.d.ts +619 -0
  67. package/dist/studio/treatment-schema.js +89 -0
  68. package/dist/studio/treatments.d.ts +31 -0
  69. package/dist/studio/treatments.js +222 -0
  70. package/dist/studio/video-review.d.ts +345 -0
  71. package/dist/studio/video-review.js +307 -0
  72. package/dist/utils/region-feedback.d.ts +2 -14
  73. package/dist/utils/region-feedback.js +1 -47
  74. package/dist/utils/regions-parser.d.ts +15 -0
  75. package/dist/utils/regions-parser.js +47 -0
  76. package/package.json +10 -4
  77. package/schemas/domotion-studio-project.schema.json +3736 -0
  78. package/schemas/storyboard-config.schema.json +8 -0
@@ -1,7 +1,10 @@
1
1
  # Deterministic fixture faces
2
2
 
3
- Subsetted fonts the frame-sequence compressor's e2e fixtures render with, so
4
- those fixtures paint the same glyph outlines on macOS, Linux, and Windows.
3
+ Deterministic fonts used by cross-platform and platform-native test fixtures.
4
+
5
+ `DomotionFixtureMono-Regular.ttf` and `DomotionFixtureSerif-Regular.ttf` are
6
+ the subsetted faces the frame-sequence compressor's e2e fixtures render with,
7
+ so those fixtures paint the same glyph outlines on macOS, Linux, and Windows.
5
8
 
6
9
  They exist because the compressor e2e tests assert pixel parity between a
7
10
  compressed run and the uncompressed flipbook of the same state, bounded by the
@@ -31,9 +34,20 @@ paint. The OFL permits redistribution of modified copies under a reserved-name
31
34
  rule, which is why the outputs carry neutral `DomotionFixture*` names rather
32
35
  than the upstream family names.
33
36
 
37
+ `DomotionProfileDevanagariOne-Regular.ttf` and
38
+ `DomotionProfileDevanagariTwo-Regular.ttf` are original, generated test fonts.
39
+ Each contains only `.notdef` and U+0905. The Windows profile/target authority
40
+ workflow installs them into the runner's real per-user font collection, then
41
+ authenticates their painted family identities through
42
+ `CSS.getPlatformFontsForNode`. They replace the Windows supplemental
43
+ language-capability installation without weakening the non-inert mutation gate.
44
+
34
45
  ## Regenerating
35
46
 
36
47
  `tools/build-fixture-fonts.mjs` rebuilds both from the upstream originals (it
37
48
  prints the download URLs). The subset is deterministic — same inputs and the
38
49
  same harfbuzzjs version produce byte-identical output, so a re-run must leave
39
50
  `git status` clean unless an input or the charset actually changed.
51
+
52
+ Run `node tools/build-generic-profile-fixture-fonts.mjs` to rebuild the two
53
+ original profile-authority faces.
@@ -12,6 +12,8 @@
12
12
  * - **embedded-font families** — `font-family:"dmf0"` in `@font-face` + the
13
13
  * `<text font-family="dmf0">` attrs. A duplicate family name makes a later
14
14
  * `@font-face` win, so text in OTHER frames reshapes to the wrong glyphs.
15
+ * Already-composed input can carry a prior namespace such as `sb0_dmf0`;
16
+ * nesting it again must namespace that whole family as one identifier.
15
17
  * - **frame / animation classes** — `class="f f-0"`, `class="anim-…"` and their
16
18
  * `.f` / `.f-0` / `.anim-…` style selectors.
17
19
  * - **`@keyframes` names** — `fv-0`, the intra-frame `f0-…` names — and the
@@ -12,6 +12,8 @@
12
12
  * - **embedded-font families** — `font-family:"dmf0"` in `@font-face` + the
13
13
  * `<text font-family="dmf0">` attrs. A duplicate family name makes a later
14
14
  * `@font-face` win, so text in OTHER frames reshapes to the wrong glyphs.
15
+ * Already-composed input can carry a prior namespace such as `sb0_dmf0`;
16
+ * nesting it again must namespace that whole family as one identifier.
15
17
  * - **frame / animation classes** — `class="f f-0"`, `class="anim-…"` and their
16
18
  * `.f` / `.f-0` / `.anim-…` style selectors.
17
19
  * - **`@keyframes` names** — `fv-0`, the intra-frame `f0-…` names — and the
@@ -49,14 +51,16 @@ export function namespaceEmbeddedAnimatedSvg(svg, token, opts = {}) {
49
51
  .replace(new RegExp(`url\\(#${e}\\)`, "g"), `url(#${token}${id})`)
50
52
  .replace(new RegExp(`((?:xlink:)?href=")#${e}(")`, "g"), `$1#${token}${id}$2`);
51
53
  }
52
- // 2. Embedded-font families (`dmfN`). Only the `@font-face` declaration and the
54
+ // 2. Embedded-font families (`dmfN`, including already-prefixed families from a
55
+ // previously composed SVG). Only the `@font-face` declaration and the
53
56
  // `font-family` attr/decl contexts — never a bare global replace, since the
54
57
  // base64 `src` payload could contain the same byte sequence. Skipped when the
55
58
  // fonts are deferred to a shared outer builder (see NamespaceEmbedOptions).
56
59
  if (opts.namespaceFonts !== false) {
60
+ const embeddedFamily = "(?:[A-Za-z][A-Za-z0-9_-]*_)?dmf\\d+";
57
61
  out = out
58
- .replace(/(font-family:\s*")(dmf\d+)(")/g, `$1${token}$2$3`)
59
- .replace(/(font-family=")(dmf\d+)(")/g, `$1${token}$2$3`);
62
+ .replace(new RegExp(`(font-family:\\s*(?:"|&quot;))(${embeddedFamily})((?:"|&quot;))`, "g"), `$1${token}$2$3`)
63
+ .replace(new RegExp(`(font-family=")(${embeddedFamily})(")`, "g"), `$1${token}$2$3`);
60
64
  }
61
65
  // 3. `@keyframes` names + their `animation:` references. A keyframe name is a
62
66
  // domotion-generated token (`fv-0`, `f0-f0a0-0`, …) that never appears as an
@@ -60,6 +60,8 @@ export interface OffsetTimelineOptions {
60
60
  startMs: number;
61
61
  /** The master loop's total period (ms). */
62
62
  masterMs: number;
63
+ /** Begin at this source-local time while preserving browser interpolation. */
64
+ contentOffsetMs?: number;
63
65
  /**
64
66
  * Timeline mode (default `"hold"`). See {@link EmbeddedTimelineMode}.
65
67
  */
@@ -154,6 +154,7 @@ export function offsetEmbeddedAnimatedSvgTimeline(svg, opts) {
154
154
  const windowMs = mode === "stretch" ? (opts.windowMs ?? periodMs) : periodMs;
155
155
  const scale = windowMs / masterMs;
156
156
  const offsetPct = (startMs / masterMs) * 100;
157
+ const contentOffsetMs = Math.max(0, opts.contentOffsetMs ?? 0);
157
158
  // Duration match tolerance — generous enough to absorb the animator's 2- vs
158
159
  // 3-decimal formatting, tight enough not to catch a distinct fixed period.
159
160
  const tol = Math.max(0.02, periodSec * 0.01);
@@ -175,7 +176,7 @@ export function offsetEmbeddedAnimatedSvgTimeline(svg, opts) {
175
176
  .replace(/style="([^"]*animation:[^"]*)"/g, (_full, css) => `style="${delayCss(css)}"`);
176
177
  }
177
178
  // Nothing to do: content already starts at the origin and fills the loop.
178
- if (offsetPct <= 1e-6 && Math.abs(scale - 1) <= 1e-6)
179
+ if (contentOffsetMs <= 0 && offsetPct <= 1e-6 && Math.abs(scale - 1) <= 1e-6)
179
180
  return svg;
180
181
  // Pass 1: retime matching animations + collect the keyframe names they drive.
181
182
  // Untouched declarations (e.g. a fixed-period cursor blink) are returned
@@ -203,13 +204,18 @@ export function offsetEmbeddedAnimatedSvgTimeline(svg, opts) {
203
204
  }
204
205
  return tokens.join(" ");
205
206
  });
206
- return changed ? `animation:${entries.join(",")}` : full;
207
+ if (!changed)
208
+ return full;
209
+ const shiftedDelay = contentOffsetMs > 0
210
+ ? `;animation-delay:${fmt((startMs - contentOffsetMs) / 1000)}s;animation-fill-mode:both`
211
+ : "";
212
+ return `animation:${entries.join(",")}${shiftedDelay}`;
207
213
  });
208
214
  let out = svg
209
215
  .replace(/<style>([\s\S]*?)<\/style>/g, (_full, css) => `<style>${retimeCss(css)}</style>`)
210
216
  .replace(/style="([^"]*animation:[^"]*)"/g, (_full, css) => `style="${retimeCss(css)}"`);
211
217
  // Pass 2: remap the keyframes those animations drive.
212
- out = out.replace(/@keyframes\s+([\w-]+)\s*\{((?:[^{}]*\{[^{}]*\})*[^{}]*)\}/g, (full, name, inner) => retimed.has(name) ? `@keyframes ${name} { ${remapKeyframeBody(inner, offsetPct, scale)} }` : full);
218
+ out = out.replace(/@keyframes\s+([\w-]+)\s*\{((?:[^{}]*\{[^{}]*\})*[^{}]*)\}/g, (full, name, inner) => retimed.has(name) ? `@keyframes ${name} { ${remapKeyframeBody(inner, contentOffsetMs > 0 ? 0 : offsetPct, scale)} }` : full);
213
219
  return out;
214
220
  }
215
221
  /** Animation-shorthand keywords that are never the `animation-name`. */
@@ -36,6 +36,8 @@ export interface AnimationFrame {
36
36
  * `offsetEmbeddedAnimatedSvgTimeline`). Omit for ordinary captured frames.
37
37
  */
38
38
  embeddedAnimationPeriodMs?: number;
39
+ /** Start this embedded animation at a source-local offset (used by storyboard trim). */
40
+ embeddedAnimationOffsetMs?: number;
39
41
  /** Transition to the next frame. The type is derived from the canonical zod schema. */
40
42
  transition?: Transition;
41
43
  /**
@@ -1132,6 +1132,7 @@ function generateAnimatedSvgBody(config) {
1132
1132
  periodMs: f.embeddedAnimationPeriodMs,
1133
1133
  startMs,
1134
1134
  masterMs: totalDuration,
1135
+ contentOffsetMs: f.embeddedAnimationOffsetMs,
1135
1136
  }),
1136
1137
  };
1137
1138
  });
@@ -1461,7 +1461,21 @@ async function buildCapturedFrame(fc, i, ctx) {
1461
1461
  cullElementsOutsideViewBox(seg.tree, scrollClip[2], scrollClip[3], undefined, 0, 1);
1462
1462
  }
1463
1463
  rootBg = segments[0]?.tree?.[0]?.styles?.rootBgComputed;
1464
- const composed = composeScrollSvg(segments, { viewportW: scrollClip[2], viewportH: scrollClip[3] });
1464
+ // The scroll composer owns a complete nested document and starts its own
1465
+ // generation, so isolate that reset from the outer animation's accumulated
1466
+ // glyph/font registries. Otherwise a scroll frame after an ordinary capture
1467
+ // rewinds gN/dmfN and replaces the earlier frame's addressing. Namespace the
1468
+ // finished document too: Blink resolves local SVG IRIs through the shared
1469
+ // TreeScope, so a nested <svg> is not an id boundary.
1470
+ const outerGeneration = snapshotGeneration();
1471
+ let composed;
1472
+ try {
1473
+ composed = composeScrollSvg(segments, { viewportW: scrollClip[2], viewportH: scrollClip[3] });
1474
+ }
1475
+ finally {
1476
+ restoreGeneration(outerGeneration);
1477
+ }
1478
+ composed = namespaceEmbeddedAnimatedSvg(composed, `sf${i}_`);
1465
1479
  // The composer emits a full `<?xml ...><svg>...</svg>` document. The
1466
1480
  // outer animator wraps `svgContent` in a `<g class="f f-N">`, which
1467
1481
  // happily contains a nested `<svg>` element — strip just the XML
@@ -11,7 +11,7 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs";
11
11
  import { basename, dirname, resolve } from "node:path";
12
12
  import { pathToFileURL } from "node:url";
13
13
  import { promisify } from "node:util";
14
- import { gzipSvg } from "../index.js";
14
+ import { gzipSvg } from "../post-processing/index.js";
15
15
  const execFileP = promisify(execFile);
16
16
  /** Parse a positive-integer flag, falling back to `def` when the flag is absent.
17
17
  * Thin default-applying wrapper over `parsePositiveInt` so the two share one
@@ -19,6 +19,117 @@
19
19
  */
20
20
  import type { Browser } from "@playwright/test";
21
21
  import { z } from "zod";
22
+ export declare const compositeLayerAnimationSchema: z.ZodObject<{
23
+ property: z.ZodEnum<{
24
+ opacity: "opacity";
25
+ transform: "transform";
26
+ scale: "scale";
27
+ translateX: "translateX";
28
+ translateY: "translateY";
29
+ clipScaleX: "clipScaleX";
30
+ clipScaleY: "clipScaleY";
31
+ }>;
32
+ from: z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>;
33
+ to: z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>;
34
+ start: z.ZodOptional<z.ZodNumber>;
35
+ duration: z.ZodOptional<z.ZodNumber>;
36
+ easing: z.ZodOptional<z.ZodString>;
37
+ transformOrigin: z.ZodOptional<z.ZodString>;
38
+ }, z.core.$strip>;
39
+ export declare const compositeLayerChromeSchema: z.ZodObject<{
40
+ device: z.ZodDefault<z.ZodEnum<{
41
+ browser: "browser";
42
+ phone: "phone";
43
+ window: "window";
44
+ }>>;
45
+ label: z.ZodOptional<z.ZodString>;
46
+ theme: z.ZodDefault<z.ZodEnum<{
47
+ light: "light";
48
+ dark: "dark";
49
+ }>>;
50
+ }, z.core.$strip>;
51
+ export declare const compositeLayerPlacementSchema: z.ZodObject<{
52
+ x: z.ZodOptional<z.ZodNumber>;
53
+ y: z.ZodOptional<z.ZodNumber>;
54
+ width: z.ZodOptional<z.ZodNumber>;
55
+ height: z.ZodOptional<z.ZodNumber>;
56
+ clip: z.ZodOptional<z.ZodBoolean>;
57
+ clipRadius: z.ZodOptional<z.ZodNumber>;
58
+ start: z.ZodOptional<z.ZodNumber>;
59
+ mode: z.ZodOptional<z.ZodEnum<{
60
+ stretch: "stretch";
61
+ hold: "hold";
62
+ loop: "loop";
63
+ }>>;
64
+ duration: z.ZodOptional<z.ZodNumber>;
65
+ animations: z.ZodOptional<z.ZodArray<z.ZodObject<{
66
+ property: z.ZodEnum<{
67
+ opacity: "opacity";
68
+ transform: "transform";
69
+ scale: "scale";
70
+ translateX: "translateX";
71
+ translateY: "translateY";
72
+ clipScaleX: "clipScaleX";
73
+ clipScaleY: "clipScaleY";
74
+ }>;
75
+ from: z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>;
76
+ to: z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>;
77
+ start: z.ZodOptional<z.ZodNumber>;
78
+ duration: z.ZodOptional<z.ZodNumber>;
79
+ easing: z.ZodOptional<z.ZodString>;
80
+ transformOrigin: z.ZodOptional<z.ZodString>;
81
+ }, z.core.$strip>>>;
82
+ }, z.core.$strip>;
83
+ export declare const compositeLayerSchema: z.ZodObject<{
84
+ x: z.ZodOptional<z.ZodNumber>;
85
+ y: z.ZodOptional<z.ZodNumber>;
86
+ width: z.ZodOptional<z.ZodNumber>;
87
+ height: z.ZodOptional<z.ZodNumber>;
88
+ clip: z.ZodOptional<z.ZodBoolean>;
89
+ clipRadius: z.ZodOptional<z.ZodNumber>;
90
+ start: z.ZodOptional<z.ZodNumber>;
91
+ mode: z.ZodOptional<z.ZodEnum<{
92
+ stretch: "stretch";
93
+ hold: "hold";
94
+ loop: "loop";
95
+ }>>;
96
+ duration: z.ZodOptional<z.ZodNumber>;
97
+ animations: z.ZodOptional<z.ZodArray<z.ZodObject<{
98
+ property: z.ZodEnum<{
99
+ opacity: "opacity";
100
+ transform: "transform";
101
+ scale: "scale";
102
+ translateX: "translateX";
103
+ translateY: "translateY";
104
+ clipScaleX: "clipScaleX";
105
+ clipScaleY: "clipScaleY";
106
+ }>;
107
+ from: z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>;
108
+ to: z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>;
109
+ start: z.ZodOptional<z.ZodNumber>;
110
+ duration: z.ZodOptional<z.ZodNumber>;
111
+ easing: z.ZodOptional<z.ZodString>;
112
+ transformOrigin: z.ZodOptional<z.ZodString>;
113
+ }, z.core.$strip>>>;
114
+ svg: z.ZodOptional<z.ZodString>;
115
+ cast: z.ZodOptional<z.ZodString>;
116
+ template: z.ZodOptional<z.ZodString>;
117
+ params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
118
+ term: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
119
+ period: z.ZodOptional<z.ZodNumber>;
120
+ chrome: z.ZodOptional<z.ZodObject<{
121
+ device: z.ZodDefault<z.ZodEnum<{
122
+ browser: "browser";
123
+ phone: "phone";
124
+ window: "window";
125
+ }>>;
126
+ label: z.ZodOptional<z.ZodString>;
127
+ theme: z.ZodDefault<z.ZodEnum<{
128
+ light: "light";
129
+ dark: "dark";
130
+ }>>;
131
+ }, z.core.$strip>>;
132
+ }, z.core.$strip>;
22
133
  export declare const compositeConfigSchema: z.ZodObject<{
23
134
  width: z.ZodNumber;
24
135
  height: z.ZodNumber;
@@ -26,24 +137,6 @@ export declare const compositeConfigSchema: z.ZodObject<{
26
137
  background: z.ZodOptional<z.ZodString>;
27
138
  duration: z.ZodOptional<z.ZodNumber>;
28
139
  layers: z.ZodArray<z.ZodObject<{
29
- svg: z.ZodOptional<z.ZodString>;
30
- cast: z.ZodOptional<z.ZodString>;
31
- template: z.ZodOptional<z.ZodString>;
32
- params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
33
- term: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
34
- period: z.ZodOptional<z.ZodNumber>;
35
- chrome: z.ZodOptional<z.ZodObject<{
36
- device: z.ZodDefault<z.ZodEnum<{
37
- browser: "browser";
38
- phone: "phone";
39
- window: "window";
40
- }>>;
41
- label: z.ZodOptional<z.ZodString>;
42
- theme: z.ZodDefault<z.ZodEnum<{
43
- light: "light";
44
- dark: "dark";
45
- }>>;
46
- }, z.core.$strip>>;
47
140
  x: z.ZodOptional<z.ZodNumber>;
48
141
  y: z.ZodOptional<z.ZodNumber>;
49
142
  width: z.ZodOptional<z.ZodNumber>;
@@ -74,9 +167,29 @@ export declare const compositeConfigSchema: z.ZodObject<{
74
167
  easing: z.ZodOptional<z.ZodString>;
75
168
  transformOrigin: z.ZodOptional<z.ZodString>;
76
169
  }, z.core.$strip>>>;
170
+ svg: z.ZodOptional<z.ZodString>;
171
+ cast: z.ZodOptional<z.ZodString>;
172
+ template: z.ZodOptional<z.ZodString>;
173
+ params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
174
+ term: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
175
+ period: z.ZodOptional<z.ZodNumber>;
176
+ chrome: z.ZodOptional<z.ZodObject<{
177
+ device: z.ZodDefault<z.ZodEnum<{
178
+ browser: "browser";
179
+ phone: "phone";
180
+ window: "window";
181
+ }>>;
182
+ label: z.ZodOptional<z.ZodString>;
183
+ theme: z.ZodDefault<z.ZodEnum<{
184
+ light: "light";
185
+ dark: "dark";
186
+ }>>;
187
+ }, z.core.$strip>>;
77
188
  }, z.core.$strip>>;
78
189
  }, z.core.$strip>;
79
190
  export type CompositeConfig = z.infer<typeof compositeConfigSchema>;
191
+ export type CompositeLayerConfig = z.infer<typeof compositeLayerSchema>;
192
+ export type CompositeLayerPlacement = z.infer<typeof compositeLayerPlacementSchema>;
80
193
  export declare function validateCompositeConfig(raw: unknown): CompositeConfig;
81
194
  /** Render + composite a validated config into one animated SVG. */
82
195
  export declare function composeCompositeConfig(browser: Browser, cfg: CompositeConfig, configDir: string, log?: (m: string) => void): Promise<string>;
@@ -27,7 +27,7 @@ import { DEVICE_CHROMES, CHROME_THEMES, wrapInDeviceChrome, clearEmbeddedFonts,
27
27
  import { composeAnimatedLayers } from "../animation/composite.js";
28
28
  import { parseSvgIntrinsicSize, detectAnimationPeriodMs } from "../animation/svg-meta.js";
29
29
  import { cliFail } from "./common.js";
30
- const layerAnimationSchema = z.object({
30
+ export const compositeLayerAnimationSchema = z.object({
31
31
  property: z.enum(["scale", "translateX", "translateY", "opacity", "transform", "clipScaleX", "clipScaleY"]),
32
32
  from: z.union([z.string(), z.number()]),
33
33
  to: z.union([z.string(), z.number()]),
@@ -36,33 +36,35 @@ const layerAnimationSchema = z.object({
36
36
  easing: z.string().optional(),
37
37
  transformOrigin: z.string().optional(),
38
38
  });
39
- const chromeSchema = z.object({
39
+ export const compositeLayerChromeSchema = z.object({
40
40
  device: z.enum(DEVICE_CHROMES).default("window"),
41
41
  label: z.string().optional(),
42
42
  theme: z.enum(CHROME_THEMES).default("dark"),
43
43
  });
44
- const layerSchema = z.object({
45
- // Exactly one source — enforced in the superRefine below.
46
- svg: z.string().optional().describe("Path to a pre-rendered SVG (static or animated)."),
47
- cast: z.string().optional().describe("Path to an asciinema v2 .cast (rendered as an animated terminal)."),
48
- template: z.string().optional().describe("A template name to render as the source."),
49
- params: z.record(z.string(), z.unknown()).optional().describe("Params for a `template` source."),
50
- term: z.record(z.string(), z.unknown()).optional().describe("Terminal options for a `cast` source."),
51
- // Period of an animated `svg` source (ms), when it can't be auto-detected.
52
- period: z.number().positive().optional(),
53
- chrome: chromeSchema.optional(),
54
- // Placement.
44
+ export const compositeLayerPlacementSchema = z.object({
55
45
  x: z.number().optional(),
56
46
  y: z.number().optional(),
57
47
  width: z.number().positive().optional(),
58
48
  height: z.number().positive().optional(),
59
49
  clip: z.boolean().optional(),
60
50
  clipRadius: z.number().nonnegative().optional(),
61
- // Timeline of the layer's own animation.
62
51
  start: z.number().nonnegative().optional(),
63
52
  mode: z.enum(["hold", "stretch", "loop"]).optional(),
64
53
  duration: z.number().positive().optional(),
65
- animations: z.array(layerAnimationSchema).optional(),
54
+ animations: z.array(compositeLayerAnimationSchema).optional(),
55
+ });
56
+ export const compositeLayerSchema = z.object({
57
+ // Exactly one source — enforced in the superRefine below.
58
+ svg: z.string().optional().describe("Path to a pre-rendered SVG (static or animated)."),
59
+ cast: z.string().optional().describe("Path to an asciinema v2 .cast (rendered as an animated terminal)."),
60
+ template: z.string().optional().describe("A template name to render as the source."),
61
+ params: z.record(z.string(), z.unknown()).optional().describe("Params for a `template` source."),
62
+ term: z.record(z.string(), z.unknown()).optional().describe("Terminal options for a `cast` source."),
63
+ // Period of an animated `svg` source (ms), when it can't be auto-detected.
64
+ period: z.number().positive().optional(),
65
+ chrome: compositeLayerChromeSchema.optional(),
66
+ // Placement.
67
+ ...compositeLayerPlacementSchema.shape,
66
68
  }).superRefine((l, ctx) => {
67
69
  const sources = [l.svg, l.cast, l.template].filter((s) => s != null);
68
70
  if (sources.length !== 1) {
@@ -75,7 +77,7 @@ export const compositeConfigSchema = z.object({
75
77
  output: z.string().optional(),
76
78
  background: z.string().optional(),
77
79
  duration: z.number().positive().optional(),
78
- layers: z.array(layerSchema).min(1),
80
+ layers: z.array(compositeLayerSchema).min(1),
79
81
  });
80
82
  export function validateCompositeConfig(raw) {
81
83
  const parsed = compositeConfigSchema.safeParse(raw);
@@ -6,6 +6,7 @@
6
6
  * domotion capture <input> [options] single-frame capture
7
7
  * domotion animate <config.json> multi-frame animated capture
8
8
  * domotion term --cast <file> terminal session → animated SVG (DM-1225)
9
+ * domotion studio [project.json] standalone Studio authoring app
9
10
  *
10
11
  * `<input>` for `capture` may be:
11
12
  * - a URL (`https://...`, `http://...`)
package/dist/cli/index.js CHANGED
@@ -6,6 +6,7 @@
6
6
  * domotion capture <input> [options] single-frame capture
7
7
  * domotion animate <config.json> multi-frame animated capture
8
8
  * domotion term --cast <file> terminal session → animated SVG (DM-1225)
9
+ * domotion studio [project.json] standalone Studio authoring app
9
10
  *
10
11
  * `<input>` for `capture` may be:
11
12
  * - a URL (`https://...`, `http://...`)
@@ -22,6 +23,7 @@ import { runTerm } from "./term.js";
22
23
  import { runTemplate } from "./template.js";
23
24
  import { runComposite } from "./composite.js";
24
25
  import { runStoryboard } from "./storyboard.js";
26
+ import { runStudio } from "./studio.js";
25
27
  // Read the version from package.json at runtime rather than hardcoding it, so
26
28
  // `domotion --version` always matches the installed package (the literal had
27
29
  // drifted to 0.1.0 while the package was at 0.5.0). Resolved relative to this
@@ -38,6 +40,7 @@ Usage:
38
40
  domotion template <name> [--param …] -o out.svg
39
41
  domotion composite <config.json> [-o out.svg]
40
42
  domotion storyboard <config.json> [-o out.svg]
43
+ domotion studio [project.json] [--workspace <dir>]
41
44
  domotion --help | --version
42
45
 
43
46
  Commands:
@@ -56,6 +59,8 @@ Commands:
56
59
  storyboard Sequence distinct SCENES (template / capture / cast / svg) end-to-end
57
60
  into one animated SVG with an inter-scene transition between each
58
61
  (title card → demo → CTA). Run 'domotion storyboard --help'.
62
+ studio Open the standalone Domotion Studio local app to create, inspect,
63
+ edit, validate, save, and reopen versioned Studio project files.
59
64
 
60
65
  capture options:
61
66
  -o, --output <path> Output SVG path (default: stdout, or <input>.svg
@@ -315,6 +320,9 @@ async function main() {
315
320
  else if (cmd === "storyboard") {
316
321
  await runStoryboard(rest);
317
322
  }
323
+ else if (cmd === "studio") {
324
+ await runStudio(rest);
325
+ }
318
326
  else {
319
327
  process.stderr.write(`domotion: unknown command "${cmd}"\n\n`);
320
328
  process.stderr.write(HELP);