domotion-svg 0.12.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/animation/animator.d.ts +26 -156
  2. package/dist/animation/animator.js +94 -15
  3. package/dist/animation/index.d.ts +1 -0
  4. package/dist/animation/index.js +4 -0
  5. package/dist/animation/overlay-schema.d.ts +261 -0
  6. package/dist/animation/overlay-schema.js +214 -0
  7. package/dist/animation/resolve-overlays.d.ts +91 -0
  8. package/dist/animation/resolve-overlays.js +101 -0
  9. package/dist/capture/content-box.d.ts +93 -0
  10. package/dist/capture/content-box.js +131 -0
  11. package/dist/capture/script/dotted-circle-detect.d.ts +3 -0
  12. package/dist/capture/script/dotted-circle-detect.js +89 -0
  13. package/dist/capture/script/emoji-detect.js +14 -0
  14. package/dist/capture/script/index.js +3 -1
  15. package/dist/capture/script/walker/form-controls.d.ts +3 -0
  16. package/dist/capture/script/walker/form-controls.js +36 -16
  17. package/dist/capture/script/walker/pseudo-content.d.ts +3 -0
  18. package/dist/capture/script/walker/pseudo-content.js +8 -0
  19. package/dist/capture/script/walker/text-segments.d.ts +3 -1
  20. package/dist/capture/script/walker/text-segments.js +72 -3
  21. package/dist/capture/script.generated.js +1 -1
  22. package/dist/capture/types.d.ts +41 -0
  23. package/dist/cli/animate.d.ts +229 -19
  24. package/dist/cli/animate.js +131 -130
  25. package/dist/cli/svg-to-video-core.d.ts +57 -3
  26. package/dist/cli/svg-to-video-core.js +202 -26
  27. package/dist/cli/svg-to-video.js +26 -5
  28. package/dist/index.d.ts +3 -0
  29. package/dist/index.js +23 -0
  30. package/dist/render/element-tree-to-svg.d.ts +19 -0
  31. package/dist/render/element-tree-to-svg.js +75 -8
  32. package/dist/render/form-controls.js +27 -5
  33. package/dist/render/text-to-path.d.ts +38 -2
  34. package/dist/render/text-to-path.js +187 -48
  35. package/dist/render/text.js +1 -1
  36. package/package.json +1 -1
  37. package/schemas/animate-config.schema.json +22 -5
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { type CursorAtResolver, type CursorOverlay, type SelectorResolver } from "./cursor-overlay.js";
8
8
  import type { MagicMove } from "./magic-move.js";
9
+ import type { AnimationOverlay, TypingOverlay, TapOverlay, SvgOverlay, BlinkOverlay, IntraFrameAnimation } from "./overlay-schema.js";
9
10
  export interface AnimationFrame {
10
11
  /** SVG content for this frame (from dom-to-svg) */
11
12
  svgContent: string;
@@ -59,162 +60,7 @@ export interface AnimationFrame {
59
60
  */
60
61
  animations?: IntraFrameAnimation[];
61
62
  }
62
- export interface TypingOverlay {
63
- kind: "typing";
64
- text: string;
65
- x: number;
66
- y: number;
67
- fontSize?: number;
68
- color?: string;
69
- /** Delay from frame start before typing begins (ms) */
70
- delay?: number;
71
- /** Speed per character (ms) */
72
- speed?: number;
73
- /** Background color to mask placeholder text */
74
- bgColor?: string;
75
- /**
76
- * Field width in px. When set, the typed text WRAPS to this width like a
77
- * browser textarea — breaking on spaces (char-breaking over-long words),
78
- * advancing one line-height per wrapped line — instead of running off the
79
- * right edge on a single line (DM-840). Omit for unbounded single-line text.
80
- */
81
- bgWidth?: number;
82
- /**
83
- * Field height in px (used to size the placeholder mask). The mask grows
84
- * beyond this if the wrapped text needs more lines, so the typed text always
85
- * sits on a clean background.
86
- */
87
- bgHeight?: number;
88
- /**
89
- * DM-870: render a blinking insertion caret. The bar sweeps the type
90
- * position while typing, then parks at the end of the text and blinks
91
- * (opacity 1↔0) until the frame ends. `true` uses defaults (the typing
92
- * `color`, 2px wide, ~530ms cadence); an object overrides them.
93
- */
94
- caret?: boolean | {
95
- color?: string;
96
- width?: number;
97
- blinkMs?: number;
98
- };
99
- }
100
- export interface TapOverlay {
101
- kind: "tap";
102
- x: number;
103
- y: number;
104
- /** Delay from frame start (ms) */
105
- delay?: number;
106
- }
107
- /**
108
- * Frame-local SVG overlay: composites a separately-captured SVG (inlined as
109
- * markup, not referenced as `<image href>`) on top of the captured frame.
110
- * Used for picture-in-picture effects like sliding a phone-framed preview
111
- * into the corner of a terminal demo.
112
- *
113
- * The overlay is positioned at (x, y), clipped to (width, height), and
114
- * gets its own `class="ov-<animId>"` wrapper so intra-frame animations
115
- * (or `enter`/`exit` sugar) can target it without colliding with elements
116
- * inside the embedded SVG.
117
- */
118
- export interface SvgOverlay {
119
- kind: "svg";
120
- /**
121
- * The SVG content to inline. The CLI resolves `src` paths from the
122
- * config file's directory and namespaces the embedded SVG's ids before
123
- * setting this field.
124
- */
125
- innerSvg: string;
126
- /** Top-left corner in the captured frame's coordinate space. */
127
- x: number;
128
- y: number;
129
- /** Render size — the embedded SVG's viewBox is preserved and scales to fit. */
130
- width: number;
131
- height: number;
132
- /**
133
- * Stable id used to key the overlay's wrapper class (`ov-<animId>`) so
134
- * `enter`/`exit` / `animations` can target it. The CLI assigns this.
135
- */
136
- animId: string;
137
- /** Slide-in entrance (DM-211). Sugar over `animations`. */
138
- enter?: {
139
- from: "top" | "bottom" | "left" | "right";
140
- duration: number;
141
- easing?: string;
142
- delay?: number;
143
- };
144
- /** Slide-out exit (DM-211). */
145
- exit?: {
146
- from: "top" | "bottom" | "left" | "right";
147
- duration: number;
148
- easing?: string;
149
- delay?: number;
150
- };
151
- }
152
- /**
153
- * DM-871: a standalone blinking bar/box, for carets/dots not tied to a typing
154
- * overlay — a recording dot, an attention pulse on a focused field, a cursor.
155
- * Renders a rect that toggles opacity on a `periodMs` cycle for the frame's
156
- * hold (sugar over a rect + a repeating opacity animation).
157
- */
158
- export interface BlinkOverlay {
159
- kind: "blink";
160
- /** Top-left corner in the captured frame's coordinate space. */
161
- x: number;
162
- y: number;
163
- width: number;
164
- height: number;
165
- /** Full on/off cycle in ms (default 1000). */
166
- periodMs?: number;
167
- /** Fill color (default a light gray). */
168
- color?: string;
169
- /** Corner radius — set to half the width/height for a dot. */
170
- radius?: number;
171
- /** Ms after the frame becomes visible before blinking starts. Default 0. */
172
- delay?: number;
173
- }
174
- export type AnimationOverlay = TypingOverlay | TapOverlay | SvgOverlay | BlinkOverlay;
175
- /**
176
- * Animate a CSS property on captured elements that match a selector, while
177
- * the frame is held on screen. The selector is resolved against the source
178
- * DOM at capture time (see DM-209) and the matching elements get
179
- * `class="anim-<id>"` on their rendered SVG groups.
180
- *
181
- * Resolution requires the consumer (CLI / `DemoRecorder`) to set
182
- * `data-domotion-anim="<id>"` on matching DOM elements before capture; the
183
- * `id` referenced here must be the same id set on the DOM.
184
- */
185
- export interface IntraFrameAnimation {
186
- /** Anim id — must match the `data-domotion-anim` value set on the DOM pre-capture. */
187
- animId: string;
188
- /**
189
- * CSS property to animate. `clipPath` takes raw CSS `clip-path` values
190
- * (e.g. `"inset(0 100% 0 0)"` -> `"inset(0 0 0 0)"`) and is the right
191
- * choice for left-to-right reveals like typing-into-captured-text. When
192
- * the captured element is wrapped in a `<g class="anim-<id>">`, the
193
- * keyframes apply `clip-path` to that wrapper.
194
- */
195
- property: "width" | "height" | "opacity" | "transform" | "translateX" | "translateY" | "clipPath";
196
- /** Start value (CSS string, e.g. `"0%"`, `"240px"`, `"0.3"`). */
197
- from: string;
198
- /** End value (same syntax as `from`). */
199
- to: string;
200
- /** Duration in ms. Must be ≤ the parent frame's `duration`. */
201
- duration: number;
202
- /** CSS easing string. Default `linear`. */
203
- easing?: string;
204
- /** Ms after the frame becomes visible before animation starts. Default 0. */
205
- delay?: number;
206
- /**
207
- * DM-869: repeat count. A positive integer or `"infinite"`. When set, the
208
- * animation loops on its own `duration` clock (CSS `animation-iteration-count`)
209
- * rather than playing once — turning a property animation into a blink / pulse
210
- * / breathe. The loop is only visible while the frame is on screen (the frame
211
- * group's visibility gating). `"infinite"` is the robust choice for a looping
212
- * scene; a finite count aligns to the frame's first appearance.
213
- */
214
- repeat?: number | "infinite";
215
- /** DM-869: when true, the loop ping-pongs `from`→`to`→`from` (CSS `animation-direction: alternate`). */
216
- alternate?: boolean;
217
- }
63
+ export type { TypingOverlay, TapOverlay, SvgOverlay, BlinkOverlay, AnimationOverlay, IntraFrameAnimation };
218
64
  export interface AnimationConfig {
219
65
  width: number;
220
66
  height: number;
@@ -260,5 +106,29 @@ export interface AnimationConfig {
260
106
  * → no rect, i.e. a transparent SVG that composites over a host background.
261
107
  */
262
108
  background?: string;
109
+ /**
110
+ * DM-1148: whether the LAST frame fades out (over its transition window) to
111
+ * dissolve back into the loop's frame 0. Default `false` — the last frame
112
+ * HOLDS solid to 100% and the loop hard-cuts to frame 0, so a one-shot video
113
+ * ends on the final frame rather than fading to nothing. Set `true` to restore
114
+ * the cross-dissolve loop (e.g. for a seamless background-loop SVG). Only
115
+ * affects the crossfade path; cut transitions already hold-then-cut.
116
+ */
117
+ loopFade?: boolean;
263
118
  }
119
+ /**
120
+ * DM-1145: re-namespace element ids that COLLIDE across frames. A caller that
121
+ * reuses identical `svgContent` for multiple frames — e.g. caching a captured
122
+ * frame and replaying it for a long hold — emits the same baked-in ids in two
123
+ * frame groups. Duplicate ids are invalid SVG: a `clip-path="url(#id)"` / filter
124
+ * / `<use href="#id">` resolves to the FIRST match, which for the later frame
125
+ * lives in an earlier (now `visibility:hidden`) frame group. Chromium renders
126
+ * that fine during continuous playback, but CLIPS THE ELEMENT AWAY when the
127
+ * timeline is SEEKED to that frame (paused + `currentTime` set) — exactly what
128
+ * svg-to-video does, so the element vanishes in the rendered video although the
129
+ * SVG looks fine when played. Rename each frame's colliding ids (and their
130
+ * in-frame references) to a frame-unique form. ONLY collisions are touched, so
131
+ * frames whose ids are already unique are emitted byte-for-byte unchanged.
132
+ */
133
+ export declare function dedupeFrameIds(frames: AnimationFrame[]): AnimationFrame[];
264
134
  export declare function generateAnimatedSvg(config: AnimationConfig): string;
@@ -118,7 +118,10 @@ function emitMagicMoveFrame(i, frame, mm, startPct, holdEndPct, transEndPct, tot
118
118
  * visible window driven by `fadeInStartPct` (precomputed by the caller, which
119
119
  * knows the overlap state). Extracted from `generateAnimatedSvg` (DM-1089).
120
120
  */
121
- function emitCrossfadeOrCutFrame(i, frame, transType, transDur, startPct, holdEndPct, transEndPct, fadeInStartPct, totalSec) {
121
+ function emitCrossfadeOrCutFrame(i, frame, transType, transDur, startPct, holdEndPct, transEndPct, fadeInStartPct, totalSec,
122
+ /** DM-1148: the last frame, when the loop must NOT cross-dissolve, holds
123
+ * opacity 1 to 100% instead of fading out over its transition window. */
124
+ holdToEnd) {
122
125
  const groups = [];
123
126
  const keyframes = [];
124
127
  groups.push(` <g class="f f-${i}">\n${frame.svgContent}\n </g>`);
@@ -143,17 +146,75 @@ function emitCrossfadeOrCutFrame(i, frame, transType, transDur, startPct, holdEn
143
146
  const prevEnd = i > 0
144
147
  ? `${padBefore(parseFloat(fadeInStartPct), KEYFRAME_EPSILON.display, 2)}%,`
145
148
  : "";
146
- keyframes.push(`
149
+ if (holdToEnd) {
150
+ // DM-1148: the final frame holds solid to 100% (no fade-out); the loop
151
+ // hard-cuts back to frame 0. The `fd` display window also runs to 100%.
152
+ // `prevEnd` (the prior frame's fade-out boundary) is empty for a lone
153
+ // frame-0 animation, which then has no opacity:0 segment at all.
154
+ const offSeg = prevEnd !== "" ? `0%, ${prevEnd.replace(/,$/, "")} { opacity: 0; }\n ` : "";
155
+ keyframes.push(`
156
+ @keyframes fv-${i} {
157
+ ${offSeg}${startPct}, 100% { opacity: 1; }
158
+ }${buildDisplayKeyframes(`fd-${i}`, fadeInStartPct, "100")}
159
+ .f-${i} { animation: fv-${i} ${totalSec.toFixed(2)}s infinite, fd-${i} ${totalSec.toFixed(2)}s infinite step-end; }`);
160
+ }
161
+ else {
162
+ keyframes.push(`
147
163
  @keyframes fv-${i} {
148
164
  0%, ${prevEnd} ${transEndPct}, 100% { opacity: 0; }
149
165
  ${startPct}, ${holdEndPct} { opacity: 1; }
150
166
  }${buildDisplayKeyframes(`fd-${i}`, fadeInStartPct, transEndPct)}
151
167
  .f-${i} { animation: fv-${i} ${totalSec.toFixed(2)}s infinite, fd-${i} ${totalSec.toFixed(2)}s infinite step-end; }`);
168
+ }
152
169
  }
153
170
  return { groups, keyframes };
154
171
  }
172
+ /**
173
+ * DM-1145: re-namespace element ids that COLLIDE across frames. A caller that
174
+ * reuses identical `svgContent` for multiple frames — e.g. caching a captured
175
+ * frame and replaying it for a long hold — emits the same baked-in ids in two
176
+ * frame groups. Duplicate ids are invalid SVG: a `clip-path="url(#id)"` / filter
177
+ * / `<use href="#id">` resolves to the FIRST match, which for the later frame
178
+ * lives in an earlier (now `visibility:hidden`) frame group. Chromium renders
179
+ * that fine during continuous playback, but CLIPS THE ELEMENT AWAY when the
180
+ * timeline is SEEKED to that frame (paused + `currentTime` set) — exactly what
181
+ * svg-to-video does, so the element vanishes in the rendered video although the
182
+ * SVG looks fine when played. Rename each frame's colliding ids (and their
183
+ * in-frame references) to a frame-unique form. ONLY collisions are touched, so
184
+ * frames whose ids are already unique are emitted byte-for-byte unchanged.
185
+ */
186
+ export function dedupeFrameIds(frames) {
187
+ const seen = new Set();
188
+ const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
189
+ return frames.map((frame, i) => {
190
+ const svg = frame.svgContent;
191
+ if (svg == null || svg === "")
192
+ return frame;
193
+ const defined = new Set();
194
+ for (const m of svg.matchAll(/\sid="([^"]+)"/g))
195
+ defined.add(m[1]);
196
+ const colliding = [...defined].filter((id) => seen.has(id));
197
+ for (const id of defined)
198
+ seen.add(id);
199
+ if (colliding.length === 0)
200
+ return frame;
201
+ let out = svg;
202
+ for (const id of colliding) {
203
+ const nid = `d${i}-${id}`;
204
+ const e = esc(id);
205
+ out = out
206
+ .replace(new RegExp(`(\\sid=")${e}(")`, "g"), `$1${nid}$2`)
207
+ .replace(new RegExp(`url\\(#${e}\\)`, "g"), `url(#${nid})`)
208
+ .replace(new RegExp(`((?:xlink:)?href=")#${e}(")`, "g"), `$1#${nid}$2`);
209
+ seen.add(nid);
210
+ }
211
+ return { ...frame, svgContent: out };
212
+ });
213
+ }
155
214
  export function generateAnimatedSvg(config) {
156
- const { width, height, frames } = config;
215
+ const { width, height } = config;
216
+ // DM-1145: guard against cross-frame id collisions (reused/cached svgContent).
217
+ const frames = dedupeFrameIds(config.frames);
157
218
  const totalDuration = frames.reduce((sum, f) => sum + frameAdvanceMs(f), 0);
158
219
  const totalSec = totalDuration / 1000;
159
220
  // Pre-compute per-frame timing windows (used by both the merge pipeline for
@@ -256,7 +317,12 @@ export function generateAnimatedSvg(config) {
256
317
  const fadeInStartPct = (i > 0 && !entersViaMagicMove)
257
318
  ? pct(Math.max(0, timeOffset - prevTransDur), totalDuration)
258
319
  : startPct;
259
- const r = emitCrossfadeOrCutFrame(i, frame, transType, transDur, startPct, holdEndPct, transEndPct, fadeInStartPct, totalSec);
320
+ // DM-1148: the last frame holds solid to 100% (no loop cross-dissolve)
321
+ // unless `loopFade` is set. Only the crossfade path fades — cut already
322
+ // holds-then-cuts — so this is a no-op for cut frames.
323
+ const isCutFrame = transType === "cut" || transDur === 0;
324
+ const holdToEnd = i === frames.length - 1 && config.loopFade !== true && !isCutFrame;
325
+ const r = emitCrossfadeOrCutFrame(i, frame, transType, transDur, startPct, holdEndPct, transEndPct, fadeInStartPct, totalSec, holdToEnd);
260
326
  frameGroups.push(...r.groups);
261
327
  keyframes.push(...r.keyframes);
262
328
  }
@@ -382,12 +448,14 @@ function renderTypingOverlay(overlay, frameIdx, frameStart, frameEnd, totalDurat
382
448
  const color = overlay.color ?? "#e6edf3";
383
449
  const typeStartMs = frameStart + delay;
384
450
  const id = `t${frameIdx}`;
385
- // DM-840: wrap to bgWidth so typed text behaves like a browser field
386
- // (textarea) — wrapping to the next line instead of running off the right
387
- // edge. Text starts at overlay.x and the bg rect starts at overlay.x-2 with
388
- // width bgWidth, so the usable text width is bgWidth-4. With no bgWidth we
389
- // keep the original single-line behavior (maxChars = Infinity).
390
- const maxLineWidth = overlay.bgWidth != null ? overlay.bgWidth - 4 : Infinity;
451
+ // DM-840 / DM-1134: wrap to `wrapWidth` so typed text behaves like a browser
452
+ // field (textarea) — wrapping to the next line instead of running off the
453
+ // right edge. Text starts at overlay.x and the bg rect starts at overlay.x-2
454
+ // with the mask width, so the usable text width is wrapWidth-4. With no wrap
455
+ // width we keep the original single-line behavior (maxChars = Infinity).
456
+ // `wrapWidth` supersedes the deprecated `bgWidth` (which fed both wrap + mask).
457
+ const wrapWidth = overlay.wrapWidth ?? overlay.bgWidth;
458
+ const maxLineWidth = wrapWidth != null ? wrapWidth - 4 : Infinity;
391
459
  const maxChars = maxLineWidth === Infinity ? Infinity : Math.max(1, Math.floor(maxLineWidth / charWidth));
392
460
  const lines = wrapTypingText(overlay.text, maxChars);
393
461
  const visibleChars = Math.max(1, lines.reduce((n, l) => n + l.length, 0));
@@ -410,11 +478,16 @@ function renderTypingOverlay(overlay, frameIdx, frameStart, frameEnd, totalDurat
410
478
  const disappearPct = pct(disappearMs, totalDuration);
411
479
  // Background mask — grown to cover every wrapped line so the typed text
412
480
  // always lands on a clean field instead of the captured placeholder.
413
- if (overlay.bgColor != null) {
414
- const bgW = overlay.bgWidth ?? longestLineChars * charWidth + 8;
415
- const bgH = Math.max(overlay.bgHeight ?? fontSize + 6, lines.length * lineHeight + 6);
481
+ // DM-1134: the mask is sized by `mask: { width, height, color }`, independent
482
+ // of the wrap width; the legacy `bgColor` / `bgWidth` / `bgHeight` are the
483
+ // deprecated fallbacks (`bgWidth` also fed the wrap above). Mask width
484
+ // defaults to the wrap width, then to the longest typed line.
485
+ const maskColor = overlay.mask?.color ?? overlay.bgColor;
486
+ if (maskColor != null) {
487
+ const bgW = overlay.mask?.width ?? wrapWidth ?? longestLineChars * charWidth + 8;
488
+ const bgH = Math.max(overlay.mask?.height ?? overlay.bgHeight ?? fontSize + 6, lines.length * lineHeight + 6);
416
489
  const bgStartPct = pct(typeStartMs, totalDuration);
417
- parts.push(` <rect class="${id}-bg" x="${overlay.x - 2}" y="${overlay.y - fontSize + 2}" width="${bgW}" height="${bgH}" fill="${overlay.bgColor}" rx="2" />`);
490
+ parts.push(` <rect class="${id}-bg" x="${overlay.x - 2}" y="${overlay.y - fontSize + 2}" width="${bgW}" height="${bgH}" fill="${maskColor}" rx="2" />`);
418
491
  cssRules.push(`
419
492
  @keyframes ${id}-bg { 0%, ${bgStartPct} { opacity: 0; } ${pct(typeStartMs + 50, totalDuration)} { opacity: 1; } ${holdEndPct} { opacity: 1; } ${disappearPct}, 100% { opacity: 0; } }
420
493
  .${id}-bg { animation: ${id}-bg ${totalSec.toFixed(2)}s infinite; }`);
@@ -441,9 +514,15 @@ function renderTypingOverlay(overlay, frameIdx, frameStart, frameEnd, totalDurat
441
514
  cumChars += line.length;
442
515
  parts.push(` <defs><clipPath id="${clipId}"><rect class="${id}-rev${li}" x="${overlay.x}" y="${lineY - fontSize}" width="0" height="${textHeight}" /></clipPath></defs>`);
443
516
  parts.push(` <text class="${id}-text" x="${overlay.x}" y="${lineY}" fill="${color}" font-size="${fontSize}" font-family="'SF Mono', Menlo, Monaco, monospace" clip-path="url(#${clipId})">${escapeHtml(line)}</text>`);
517
+ // DM-1204: the reveal clip MUST sweep linearly so its right edge tracks the
518
+ // caret (whose position track is `linear`). Without an explicit timing
519
+ // function the width animation defaults to CSS `ease`, which races ~80%
520
+ // through the sweep at the time-midpoint while the linear caret is only at
521
+ // 50% — that desync read as the caret lagging ~10–20 chars behind the
522
+ // revealed text mid-type, even though the endpoints (parked state) matched.
444
523
  cssRules.push(`
445
524
  @keyframes ${id}-rev${li} { 0%, ${lineStartPct} { width: 0; } ${lineEndPct} { width: ${lineWidth}px; } ${holdEndPct} { width: ${lineWidth}px; } ${disappearPct}, 100% { width: 0; } }
446
- .${id}-rev${li} { animation: ${id}-rev${li} ${totalSec.toFixed(2)}s infinite; }`);
525
+ .${id}-rev${li} { animation: ${id}-rev${li} ${totalSec.toFixed(2)}s linear infinite; }`);
447
526
  });
448
527
  // Whole-overlay visibility — shared by every line's <text>.
449
528
  const typeStartPct = pct(typeStartMs, totalDuration);
@@ -2,3 +2,4 @@ export { buildMagicMove, type MagicMove, type MagicMoveSlide, } from "./magic-mo
2
2
  export { generateAnimatedSvg, type AnimationConfig, type AnimationFrame, type AnimationOverlay, type TypingOverlay, type TapOverlay, type SvgOverlay, type IntraFrameAnimation, } from "./animator.js";
3
3
  export { cursorOverlayMarkup, resolveCursorScript, cursorAtPoint, type CursorOverlay, type CursorEvent, type CursorMoveEvent, type CursorClickEvent, type CursorShowEvent, type CursorHideEvent, type CursorStyle, type CursorTimelineEntry, type SelectorResolver, type CursorAtResolver, } from "./cursor-overlay.js";
4
4
  export { CURSOR_GLYPHS, CURSOR_CATEGORIES, cursorGlyphSvg, type CursorGlyph } from "./cursor-glyphs.js";
5
+ export { resolveOverlays, type OverlayAnchor, type AnchoredOverlay } from "./resolve-overlays.js";
@@ -7,3 +7,7 @@ export { buildMagicMove, } from "./magic-move.js";
7
7
  export { generateAnimatedSvg, } from "./animator.js";
8
8
  export { cursorOverlayMarkup, resolveCursorScript, cursorAtPoint, } from "./cursor-overlay.js";
9
9
  export { CURSOR_GLYPHS, CURSOR_CATEGORIES, cursorGlyphSvg } from "./cursor-glyphs.js";
10
+ // DM-1132: lower selector-anchored overlays into concrete-coordinate overlays
11
+ // against a live page — the resolution step the declarative CLI uses, now
12
+ // reachable by imperative `captureElementTree` + `generateAnimatedSvg` callers.
13
+ export { resolveOverlays } from "./resolve-overlays.js";
@@ -0,0 +1,261 @@
1
+ /**
2
+ * Single source of truth for the animation overlay + intra-frame-animation
3
+ * shapes (DM-1131).
4
+ *
5
+ * These zod schemas define the *resolved* (runtime) overlay shapes that
6
+ * `generateAnimatedSvg` consumes — concrete `x` / `y` / `bgWidth`, no
7
+ * selector `anchor`. The TypeScript types the public API exports
8
+ * (`TypingOverlay`, `AnimationOverlay`, …) are derived from them via
9
+ * `z.infer`, so there is exactly ONE definition of each shape.
10
+ *
11
+ * The declarative-config layer (`src/cli/animate.ts`) builds its *authoring*
12
+ * overlay schemas by EXTENDING these base schemas (adding `anchor` /
13
+ * `maxWidth`, relaxing `x` / `y` to defaulted), so the two views can no longer
14
+ * drift: rename a field here and both the renderer types and the config
15
+ * validator (plus its generated JSON Schema) move together, or fail to
16
+ * compile. Before this split the renderer hand-wrote the interfaces and the
17
+ * CLI hand-wrote a parallel zod schema — a rename on one side was invisible to
18
+ * the other (the `Overlay` → `AnimationOverlay` regression that motivated this).
19
+ *
20
+ * Field docs live here as comments; the consumer-facing contract is
21
+ * `docs/api.md` + `docs/08-animation-model.md` / `docs/43-declarative-animate-
22
+ * config.md`. Keep this file's field set in lockstep with those docs.
23
+ */
24
+ import { z } from "zod";
25
+ /**
26
+ * Slide-in / slide-out descriptor shared by SVG overlays (`enter` / `exit`)
27
+ * — DM-211. Sugar over an intra-frame animation.
28
+ */
29
+ export declare const overlaySlideSchema: z.ZodObject<{
30
+ from: z.ZodEnum<{
31
+ left: "left";
32
+ right: "right";
33
+ top: "top";
34
+ bottom: "bottom";
35
+ }>;
36
+ duration: z.ZodNumber;
37
+ easing: z.ZodOptional<z.ZodString>;
38
+ delay: z.ZodOptional<z.ZodNumber>;
39
+ }, z.core.$strip>;
40
+ export type OverlaySlide = z.infer<typeof overlaySlideSchema>;
41
+ /**
42
+ * The placeholder cover painted behind a typing overlay's text, sized
43
+ * independently of the wrap width (DM-1134). All three fields are optional:
44
+ * `width` defaults to the wrap width (then to the longest typed line),
45
+ * `height` grows to fit the wrapped lines, and the mask only paints when a
46
+ * `color` is resolvable.
47
+ */
48
+ export declare const typingMaskSchema: z.ZodObject<{
49
+ width: z.ZodOptional<z.ZodNumber>;
50
+ height: z.ZodOptional<z.ZodNumber>;
51
+ color: z.ZodOptional<z.ZodString>;
52
+ }, z.core.$strip>;
53
+ export type TypingMask = z.infer<typeof typingMaskSchema>;
54
+ /**
55
+ * A typed-text reveal layered onto a captured frame.
56
+ *
57
+ * DM-1134: wrapping and the placeholder mask are now separate knobs —
58
+ * `wrapWidth` controls where the text line-breaks (browser-textarea style,
59
+ * DM-840) and `mask: { width, height, color }` controls the cover. The legacy
60
+ * `bgWidth` / `bgHeight` / `bgColor` fields still work (deprecated aliases):
61
+ * `bgWidth` feeds both `wrapWidth` and `mask.width`, `bgHeight` → `mask.height`,
62
+ * `bgColor` → `mask.color`. Prefer the new fields in new code.
63
+ */
64
+ export declare const typingOverlaySchema: z.ZodObject<{
65
+ kind: z.ZodLiteral<"typing">;
66
+ text: z.ZodString;
67
+ x: z.ZodNumber;
68
+ y: z.ZodNumber;
69
+ fontSize: z.ZodOptional<z.ZodNumber>;
70
+ color: z.ZodOptional<z.ZodString>;
71
+ delay: z.ZodOptional<z.ZodNumber>;
72
+ speed: z.ZodOptional<z.ZodNumber>;
73
+ wrapWidth: z.ZodOptional<z.ZodNumber>;
74
+ mask: z.ZodOptional<z.ZodObject<{
75
+ width: z.ZodOptional<z.ZodNumber>;
76
+ height: z.ZodOptional<z.ZodNumber>;
77
+ color: z.ZodOptional<z.ZodString>;
78
+ }, z.core.$strip>>;
79
+ bgColor: z.ZodOptional<z.ZodString>;
80
+ bgWidth: z.ZodOptional<z.ZodNumber>;
81
+ bgHeight: z.ZodOptional<z.ZodNumber>;
82
+ caret: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodObject<{
83
+ color: z.ZodOptional<z.ZodString>;
84
+ width: z.ZodOptional<z.ZodNumber>;
85
+ blinkMs: z.ZodOptional<z.ZodNumber>;
86
+ }, z.core.$strip>]>>;
87
+ }, z.core.$strip>;
88
+ export type TypingOverlay = z.infer<typeof typingOverlaySchema>;
89
+ /** A tap-ripple at `(x, y)`, frame-relative. */
90
+ export declare const tapOverlaySchema: z.ZodObject<{
91
+ kind: z.ZodLiteral<"tap">;
92
+ x: z.ZodNumber;
93
+ y: z.ZodNumber;
94
+ delay: z.ZodOptional<z.ZodNumber>;
95
+ }, z.core.$strip>;
96
+ export type TapOverlay = z.infer<typeof tapOverlaySchema>;
97
+ /**
98
+ * Frame-local SVG overlay: composites a separately-captured SVG (inlined as
99
+ * markup, not referenced as `<image href>`) on top of the captured frame.
100
+ * Used for picture-in-picture effects like sliding a phone-framed preview
101
+ * into the corner of a terminal demo.
102
+ *
103
+ * The overlay is positioned at (x, y), clipped to (width, height), and gets
104
+ * its own `class="ov-<animId>"` wrapper so intra-frame animations (or
105
+ * `enter`/`exit` sugar) can target it without colliding with elements inside
106
+ * the embedded SVG.
107
+ *
108
+ * Note: this is the RESOLVED shape — the declarative config takes a `src` file
109
+ * path and the CLI reads / namespaces it into `innerSvg` + `animId`.
110
+ */
111
+ export declare const svgOverlaySchema: z.ZodObject<{
112
+ kind: z.ZodLiteral<"svg">;
113
+ innerSvg: z.ZodString;
114
+ x: z.ZodNumber;
115
+ y: z.ZodNumber;
116
+ width: z.ZodNumber;
117
+ height: z.ZodNumber;
118
+ animId: z.ZodString;
119
+ enter: z.ZodOptional<z.ZodObject<{
120
+ from: z.ZodEnum<{
121
+ left: "left";
122
+ right: "right";
123
+ top: "top";
124
+ bottom: "bottom";
125
+ }>;
126
+ duration: z.ZodNumber;
127
+ easing: z.ZodOptional<z.ZodString>;
128
+ delay: z.ZodOptional<z.ZodNumber>;
129
+ }, z.core.$strip>>;
130
+ exit: z.ZodOptional<z.ZodObject<{
131
+ from: z.ZodEnum<{
132
+ left: "left";
133
+ right: "right";
134
+ top: "top";
135
+ bottom: "bottom";
136
+ }>;
137
+ duration: z.ZodNumber;
138
+ easing: z.ZodOptional<z.ZodString>;
139
+ delay: z.ZodOptional<z.ZodNumber>;
140
+ }, z.core.$strip>>;
141
+ }, z.core.$strip>;
142
+ export type SvgOverlay = z.infer<typeof svgOverlaySchema>;
143
+ /**
144
+ * DM-871: a standalone blinking bar/box, for carets/dots not tied to a typing
145
+ * overlay — a recording dot, an attention pulse on a focused field, a cursor.
146
+ * Renders a rect that toggles opacity on a `periodMs` cycle for the frame's
147
+ * hold (sugar over a rect + a repeating opacity animation).
148
+ */
149
+ export declare const blinkOverlaySchema: z.ZodObject<{
150
+ kind: z.ZodLiteral<"blink">;
151
+ x: z.ZodNumber;
152
+ y: z.ZodNumber;
153
+ width: z.ZodNumber;
154
+ height: z.ZodNumber;
155
+ periodMs: z.ZodOptional<z.ZodNumber>;
156
+ color: z.ZodOptional<z.ZodString>;
157
+ radius: z.ZodOptional<z.ZodNumber>;
158
+ delay: z.ZodOptional<z.ZodNumber>;
159
+ }, z.core.$strip>;
160
+ export type BlinkOverlay = z.infer<typeof blinkOverlaySchema>;
161
+ /** The resolved overlay union `generateAnimatedSvg` consumes per frame. */
162
+ export declare const animationOverlaySchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
163
+ kind: z.ZodLiteral<"typing">;
164
+ text: z.ZodString;
165
+ x: z.ZodNumber;
166
+ y: z.ZodNumber;
167
+ fontSize: z.ZodOptional<z.ZodNumber>;
168
+ color: z.ZodOptional<z.ZodString>;
169
+ delay: z.ZodOptional<z.ZodNumber>;
170
+ speed: z.ZodOptional<z.ZodNumber>;
171
+ wrapWidth: z.ZodOptional<z.ZodNumber>;
172
+ mask: z.ZodOptional<z.ZodObject<{
173
+ width: z.ZodOptional<z.ZodNumber>;
174
+ height: z.ZodOptional<z.ZodNumber>;
175
+ color: z.ZodOptional<z.ZodString>;
176
+ }, z.core.$strip>>;
177
+ bgColor: z.ZodOptional<z.ZodString>;
178
+ bgWidth: z.ZodOptional<z.ZodNumber>;
179
+ bgHeight: z.ZodOptional<z.ZodNumber>;
180
+ caret: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodObject<{
181
+ color: z.ZodOptional<z.ZodString>;
182
+ width: z.ZodOptional<z.ZodNumber>;
183
+ blinkMs: z.ZodOptional<z.ZodNumber>;
184
+ }, z.core.$strip>]>>;
185
+ }, z.core.$strip>, z.ZodObject<{
186
+ kind: z.ZodLiteral<"tap">;
187
+ x: z.ZodNumber;
188
+ y: z.ZodNumber;
189
+ delay: z.ZodOptional<z.ZodNumber>;
190
+ }, z.core.$strip>, z.ZodObject<{
191
+ kind: z.ZodLiteral<"svg">;
192
+ innerSvg: z.ZodString;
193
+ x: z.ZodNumber;
194
+ y: z.ZodNumber;
195
+ width: z.ZodNumber;
196
+ height: z.ZodNumber;
197
+ animId: z.ZodString;
198
+ enter: z.ZodOptional<z.ZodObject<{
199
+ from: z.ZodEnum<{
200
+ left: "left";
201
+ right: "right";
202
+ top: "top";
203
+ bottom: "bottom";
204
+ }>;
205
+ duration: z.ZodNumber;
206
+ easing: z.ZodOptional<z.ZodString>;
207
+ delay: z.ZodOptional<z.ZodNumber>;
208
+ }, z.core.$strip>>;
209
+ exit: z.ZodOptional<z.ZodObject<{
210
+ from: z.ZodEnum<{
211
+ left: "left";
212
+ right: "right";
213
+ top: "top";
214
+ bottom: "bottom";
215
+ }>;
216
+ duration: z.ZodNumber;
217
+ easing: z.ZodOptional<z.ZodString>;
218
+ delay: z.ZodOptional<z.ZodNumber>;
219
+ }, z.core.$strip>>;
220
+ }, z.core.$strip>, z.ZodObject<{
221
+ kind: z.ZodLiteral<"blink">;
222
+ x: z.ZodNumber;
223
+ y: z.ZodNumber;
224
+ width: z.ZodNumber;
225
+ height: z.ZodNumber;
226
+ periodMs: z.ZodOptional<z.ZodNumber>;
227
+ color: z.ZodOptional<z.ZodString>;
228
+ radius: z.ZodOptional<z.ZodNumber>;
229
+ delay: z.ZodOptional<z.ZodNumber>;
230
+ }, z.core.$strip>], "kind">;
231
+ export type AnimationOverlay = z.infer<typeof animationOverlaySchema>;
232
+ /**
233
+ * Animate a CSS property on captured elements that match a selector, while the
234
+ * frame is held on screen. The selector is resolved against the source DOM at
235
+ * capture time (see DM-209) and the matching elements get `class="anim-<id>"`
236
+ * on their rendered SVG groups.
237
+ *
238
+ * Resolution requires the consumer (CLI / `DemoRecorder`) to set
239
+ * `data-domotion-anim="<id>"` on matching DOM elements before capture; the
240
+ * `id` referenced here must be the same id set on the DOM.
241
+ */
242
+ export declare const intraFrameAnimationSchema: z.ZodObject<{
243
+ animId: z.ZodString;
244
+ property: z.ZodEnum<{
245
+ width: "width";
246
+ clipPath: "clipPath";
247
+ height: "height";
248
+ transform: "transform";
249
+ opacity: "opacity";
250
+ translateX: "translateX";
251
+ translateY: "translateY";
252
+ }>;
253
+ from: z.ZodString;
254
+ to: z.ZodString;
255
+ duration: z.ZodNumber;
256
+ easing: z.ZodOptional<z.ZodString>;
257
+ delay: z.ZodOptional<z.ZodNumber>;
258
+ repeat: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodLiteral<"infinite">]>>;
259
+ alternate: z.ZodOptional<z.ZodBoolean>;
260
+ }, z.core.$strip>;
261
+ export type IntraFrameAnimation = z.infer<typeof intraFrameAnimationSchema>;