domotion-svg 0.11.0 → 0.13.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/animation/animator.d.ts +26 -156
- package/dist/animation/animator.js +87 -14
- package/dist/animation/index.d.ts +1 -0
- package/dist/animation/index.js +4 -0
- package/dist/animation/overlay-schema.d.ts +261 -0
- package/dist/animation/overlay-schema.js +214 -0
- package/dist/animation/resolve-overlays.d.ts +91 -0
- package/dist/animation/resolve-overlays.js +101 -0
- package/dist/capture/content-box.d.ts +93 -0
- package/dist/capture/content-box.js +131 -0
- package/dist/capture/script/dotted-circle-detect.d.ts +3 -0
- package/dist/capture/script/dotted-circle-detect.js +89 -0
- package/dist/capture/script/emoji-detect.js +14 -0
- package/dist/capture/script/index.js +12 -1
- package/dist/capture/script/walker/form-controls.d.ts +3 -0
- package/dist/capture/script/walker/form-controls.js +49 -18
- package/dist/capture/script/walker/pseudo-content.d.ts +3 -0
- package/dist/capture/script/walker/pseudo-content.js +8 -0
- package/dist/capture/script/walker/text-segments.d.ts +3 -1
- package/dist/capture/script/walker/text-segments.js +72 -3
- package/dist/capture/script.generated.js +1 -1
- package/dist/capture/types.d.ts +50 -0
- package/dist/cli/animate.d.ts +229 -19
- package/dist/cli/animate.js +131 -130
- package/dist/cli/svg-to-video-core.d.ts +57 -3
- package/dist/cli/svg-to-video-core.js +202 -26
- package/dist/cli/svg-to-video.js +26 -5
- package/dist/index.d.ts +3 -0
- package/dist/index.js +23 -0
- package/dist/render/element-tree-to-svg.d.ts +26 -0
- package/dist/render/element-tree-to-svg.js +179 -15
- package/dist/render/form-controls.js +27 -5
- package/dist/render/text-to-path.d.ts +38 -2
- package/dist/render/text-to-path.js +243 -49
- package/dist/render/text.d.ts +1 -0
- package/dist/render/text.js +46 -3
- package/dist/render/vertical-text.js +36 -2
- package/package.json +1 -1
- 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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
386
|
-
// (textarea) — wrapping to the next line instead of running off the
|
|
387
|
-
// edge. Text starts at overlay.x and the bg rect starts at overlay.x-2
|
|
388
|
-
// width
|
|
389
|
-
// keep the original single-line behavior (maxChars = Infinity).
|
|
390
|
-
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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="${
|
|
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; }`);
|
|
@@ -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";
|
package/dist/animation/index.js
CHANGED
|
@@ -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>;
|