@yassine-bouassida/scenecap 1.0.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/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright 2026 Yassine Bouassida
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,183 @@
1
+ # Scenecap
2
+
3
+ Record scripted screen scenarios as MP4/WebM videos directly from your React app — entirely in the browser, zero backend.
4
+
5
+ Describe what should happen in **plain English**, and Scenecap parses it, executes the actions on-screen (virtual cursor, animations, zoom, annotations), and captures everything into a downloadable video file.
6
+
7
+ > Think of it as "Playwright meets screen recorder meets annotation tool" — but running entirely in the browser.
8
+
9
+ ---
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ npm install scenecap
15
+ ```
16
+
17
+ ---
18
+
19
+ ## Quick Start
20
+
21
+ ### React Hook (recommended for Next.js)
22
+
23
+ ```tsx
24
+ 'use client';
25
+ import { useScenecap } from 'scenecap';
26
+
27
+ export default function MyPage() {
28
+ const { containerRef, record, download, isRecording, progress } = useScenecap();
29
+
30
+ const handleRecord = async () => {
31
+ await record(`
32
+ Click the "Login" button
33
+ Type "user@mail.com" in the email input
34
+ Zoom in on the password field
35
+ Circle the submit button in red
36
+ `);
37
+ download('my-demo.webm');
38
+ };
39
+
40
+ return (
41
+ <>
42
+ <button onClick={handleRecord} disabled={isRecording}>
43
+ {isRecording ? `${Math.round(progress)}%` : 'Record'}
44
+ </button>
45
+ <div ref={containerRef}>
46
+ {/* your UI */}
47
+ </div>
48
+ </>
49
+ );
50
+ }
51
+ ```
52
+
53
+ ### Vanilla JS / Imperative
54
+
55
+ ```ts
56
+ import { createScenecap } from 'scenecap';
57
+
58
+ const sc = createScenecap({ recording: { fps: 30 } });
59
+ const blob = await sc.record(`
60
+ Click the signup button
61
+ Type "hello" in the name field
62
+ `, document.getElementById('app')!);
63
+
64
+ const a = document.createElement('a');
65
+ a.href = URL.createObjectURL(blob);
66
+ a.download = 'recording.webm';
67
+ a.click();
68
+ ```
69
+
70
+ ### Structured Scenario (skip NLP)
71
+
72
+ ```ts
73
+ import { ScenarioRunner } from 'scenecap';
74
+
75
+ const runner = new ScenarioRunner();
76
+ const blob = await runner.recordScenario({
77
+ name: 'Login Flow',
78
+ actions: [
79
+ { type: 'click', target: '#login-btn' },
80
+ { type: 'type', target: 'input[name="email"]', value: 'test@test.com' },
81
+ { type: 'zoom', target: '#submit', zoomLevel: 2.0 },
82
+ { type: 'circle', target: '#submit', color: '#ff0000' },
83
+ ],
84
+ }, containerEl);
85
+ ```
86
+
87
+ ---
88
+
89
+ ## Supported Actions
90
+
91
+ | What you write | What it does |
92
+ |---|---|
93
+ | `Click the "Submit" button` | Clicks element by text content |
94
+ | `Type "hello" in the email field` | Keystroke-by-keystroke typing |
95
+ | `Zoom in on the navbar to 200%` | Smooth CSS transform zoom |
96
+ | `Zoom out` | Resets zoom to 100% |
97
+ | `Circle the logo in red` | Animated ellipse with glow effect |
98
+ | `Highlight the error message in yellow` | Semi-transparent overlay |
99
+ | `Draw an arrow to the submit button` | Animated arrow pointing to element |
100
+ | `Add text "Click here!" near the button` | Pill-shaped label |
101
+ | `Scroll down 500px` | Smooth scroll by pixel amount |
102
+ | `Scroll to the footer` | Scrolls element into view |
103
+ | `Hover over the menu` | Dispatches mouseenter/mouseover |
104
+ | `Wait 2 seconds` | Pauses execution |
105
+ | `Navigate to /dashboard` | Changes window location |
106
+ | `Take a screenshot` | White flash effect |
107
+
108
+ ---
109
+
110
+ ## Configuration
111
+
112
+ ```ts
113
+ createScenecap({
114
+ recording: {
115
+ format: 'webm', // 'webm' | 'mp4' (mp4 depends on browser support)
116
+ fps: 30,
117
+ videoBitrate: 5_000_000,
118
+ width: 1280,
119
+ height: 720,
120
+ audio: false,
121
+ backgroundColor: '#ffffff',
122
+ devicePixelRatio: 1,
123
+ },
124
+ annotations: {
125
+ circleColor: '#ff3b30',
126
+ highlightColor: '#ffcc02',
127
+ arrowColor: '#ff3b30',
128
+ textColor: '#1a1a1a',
129
+ thickness: 3,
130
+ },
131
+ animation: {
132
+ defaultDuration: 800, // ms per action
133
+ defaultEasing: 'ease-in-out',
134
+ zoomDuration: 600,
135
+ },
136
+ callbacks: {
137
+ onActionStart: (action, index) => {},
138
+ onActionComplete: (action, index) => {},
139
+ onProgress: (percent) => {},
140
+ onError: (error) => {},
141
+ onComplete: (blob) => {},
142
+ },
143
+ });
144
+ ```
145
+
146
+ ---
147
+
148
+ ## Browser Requirements
149
+
150
+ | API | Minimum versions |
151
+ |---|---|
152
+ | MediaRecorder | Chrome 49+, Firefox 25+, Edge 79+, Safari 14.1+ |
153
+ | Canvas captureStream | Chrome 51+, Firefox 43+, Edge 79+, Safari 14.1+ |
154
+
155
+ Does **not** work in Node.js / SSR — all recording logic is browser-only.
156
+
157
+ ---
158
+
159
+ ## Known Limitations
160
+
161
+ - **DOM fidelity**: The SVG foreignObject approach doesn't capture cross-origin images, iframes, or some CSS effects (backdrop-filter, complex shadows).
162
+ - **MP4 output**: MediaRecorder natively outputs WebM. True MP4 requires ffmpeg.wasm (not yet integrated).
163
+ - **NLP parser**: Regex-based — handles common phrasings well but won't understand arbitrary sentences.
164
+ - **No iframe support**: Can only record elements within the same document.
165
+ - **Single-tab only**: Cross-page navigations will break the recording session.
166
+
167
+ ---
168
+
169
+ ## Development
170
+
171
+ ```bash
172
+ npm install
173
+ npm run build # compile to dist/ (CJS + ESM + types)
174
+ npm run dev # watch mode
175
+ npm run lint
176
+ npm run test
177
+ ```
178
+
179
+ ---
180
+
181
+ ## License
182
+
183
+ MIT — free for personal and commercial use.
@@ -0,0 +1,376 @@
1
+ type ActionType = 'click' | 'type' | 'scroll' | 'hover' | 'wait' | 'zoom' | 'circle' | 'highlight' | 'arrow' | 'annotate' | 'navigate' | 'screenshot';
2
+ interface ScenarioAction {
3
+ /** The type of action to perform */
4
+ type: ActionType;
5
+ /** CSS selector or natural language description of the target element */
6
+ target?: string;
7
+ /** Value for type actions, text for annotations, URL for navigate */
8
+ value?: string;
9
+ /** Duration in milliseconds */
10
+ duration?: number;
11
+ /** Delay before executing this action (ms) */
12
+ delay?: number;
13
+ /** Zoom level for zoom actions (1.0 = 100%) */
14
+ zoomLevel?: number;
15
+ /** Color for annotations/highlights (CSS color string) */
16
+ color?: string;
17
+ /** Thickness for circles/arrows (px) */
18
+ thickness?: number;
19
+ /** Position offset for annotations */
20
+ offset?: {
21
+ x: number;
22
+ y: number;
23
+ };
24
+ /** Easing function for animations */
25
+ easing?: 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out';
26
+ }
27
+ interface Scenario {
28
+ /** Unique name for this scenario */
29
+ name: string;
30
+ /** Description of what this scenario demonstrates */
31
+ description?: string;
32
+ /** The URL to start recording from */
33
+ startUrl?: string;
34
+ /** Viewport width */
35
+ viewportWidth?: number;
36
+ /** Viewport height */
37
+ viewportHeight?: number;
38
+ /** Ordered list of actions to perform */
39
+ actions: ScenarioAction[];
40
+ }
41
+ interface RecordingOptions {
42
+ /** Output format */
43
+ format: 'webm' | 'mp4';
44
+ /** Frames per second */
45
+ fps: number;
46
+ /** Video bitrate in bits/s */
47
+ videoBitrate: number;
48
+ /** Viewport dimensions */
49
+ width: number;
50
+ height: number;
51
+ /** Whether to record audio */
52
+ audio: boolean;
53
+ /** Background color */
54
+ backgroundColor: string;
55
+ /** Device pixel ratio (for retina) */
56
+ devicePixelRatio: number;
57
+ }
58
+ interface RecordingSession {
59
+ /** Unique session ID */
60
+ id: string;
61
+ /** The scenario being recorded */
62
+ scenario: Scenario;
63
+ /** Recording options */
64
+ options: RecordingOptions;
65
+ /** Current status */
66
+ status: 'idle' | 'preparing' | 'recording' | 'processing' | 'complete' | 'error';
67
+ /** Progress 0-100 */
68
+ progress: number;
69
+ /** Error message if failed */
70
+ error?: string;
71
+ /** The final video blob */
72
+ result?: Blob;
73
+ }
74
+ interface OverlayAnnotation {
75
+ id: string;
76
+ type: 'circle' | 'highlight' | 'arrow' | 'text' | 'zoom-indicator';
77
+ target: DOMRect;
78
+ color: string;
79
+ thickness: number;
80
+ text?: string;
81
+ opacity: number;
82
+ /** Animation progress 0-1 */
83
+ animProgress: number;
84
+ }
85
+ interface ParsedInstruction {
86
+ raw: string;
87
+ action: ScenarioAction;
88
+ confidence: number;
89
+ }
90
+ interface ScenecapCallbacks {
91
+ onActionStart?: (action: ScenarioAction, index: number) => void;
92
+ onActionComplete?: (action: ScenarioAction, index: number) => void;
93
+ onProgress?: (progress: number) => void;
94
+ onError?: (error: Error) => void;
95
+ onComplete?: (blob: Blob) => void;
96
+ }
97
+ interface ScenecapConfig {
98
+ /** Default recording options */
99
+ recording?: Partial<RecordingOptions>;
100
+ /** Default annotation styles */
101
+ annotations?: {
102
+ circleColor?: string;
103
+ highlightColor?: string;
104
+ arrowColor?: string;
105
+ textColor?: string;
106
+ thickness?: number;
107
+ fontFamily?: string;
108
+ fontSize?: number;
109
+ };
110
+ /** Animation defaults */
111
+ animation?: {
112
+ defaultDuration?: number;
113
+ defaultEasing?: 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out';
114
+ zoomDuration?: number;
115
+ };
116
+ /** Callbacks */
117
+ callbacks?: ScenecapCallbacks;
118
+ }
119
+
120
+ declare class ScenarioRunner {
121
+ private config;
122
+ private callbacks;
123
+ private session;
124
+ private overlay;
125
+ private recorder;
126
+ private zoom;
127
+ private cursor;
128
+ constructor(config?: Partial<ScenecapConfig>);
129
+ /**
130
+ * Record a scenario described in natural language.
131
+ *
132
+ * @param script - Natural language description of the scenario
133
+ * @param container - The DOM element to record
134
+ * @param options - Additional scenario options
135
+ * @returns The recorded video as a Blob
136
+ *
137
+ * @example
138
+ * ```ts
139
+ * const blob = await runner.record(`
140
+ * Click on the "Sign Up" button
141
+ * Type "john@email.com" in the email field
142
+ * Zoom in on the submit button
143
+ * Circle the success message in green
144
+ * `, document.getElementById('app')!);
145
+ * ```
146
+ */
147
+ record(script: string, container: HTMLElement, options?: {
148
+ name?: string;
149
+ description?: string;
150
+ startUrl?: string;
151
+ }): Promise<Blob>;
152
+ /**
153
+ * Record a structured scenario object.
154
+ */
155
+ recordScenario(scenario: Scenario, container: HTMLElement): Promise<Blob>;
156
+ /**
157
+ * Parse a natural language script without recording (for previewing).
158
+ */
159
+ parse(script: string): Scenario;
160
+ /**
161
+ * Get current session info.
162
+ */
163
+ getSession(): RecordingSession | null;
164
+ private executeAction;
165
+ private cleanup;
166
+ }
167
+
168
+ /**
169
+ * Records a DOM element to a video file using the Canvas capture API.
170
+ * This captures the visual output of a container element frame-by-frame.
171
+ */
172
+ declare class VideoRecorder {
173
+ private options;
174
+ private mediaRecorder;
175
+ private chunks;
176
+ private canvas;
177
+ private stream;
178
+ private captureInterval;
179
+ constructor(options?: Partial<RecordingOptions>);
180
+ /**
181
+ * Start recording from a canvas element.
182
+ */
183
+ startFromCanvas(canvas: HTMLCanvasElement): Promise<void>;
184
+ /**
185
+ * Start recording a DOM element by continuously painting it to a canvas.
186
+ */
187
+ startFromElement(element: HTMLElement): Promise<void>;
188
+ /**
189
+ * Stop recording and return the video blob.
190
+ */
191
+ stop(): Promise<Blob>;
192
+ private cleanup;
193
+ get isRecording(): boolean;
194
+ }
195
+
196
+ /**
197
+ * Manages viewport zoom transformations during recording.
198
+ * Uses CSS transforms on a container for smooth, GPU-accelerated zooming.
199
+ */
200
+ declare class ZoomController {
201
+ private container;
202
+ private currentZoom;
203
+ private currentTranslateX;
204
+ private currentTranslateY;
205
+ constructor(container: HTMLElement);
206
+ /**
207
+ * Zoom into a specific element.
208
+ */
209
+ zoomTo(target: DOMRect, level?: number, duration?: number): Promise<void>;
210
+ /**
211
+ * Reset zoom to default.
212
+ */
213
+ resetZoom(duration?: number): Promise<void>;
214
+ get zoom(): number;
215
+ destroy(): void;
216
+ }
217
+
218
+ declare class AnnotationOverlay {
219
+ private canvas;
220
+ private ctx;
221
+ private annotations;
222
+ private animationFrame;
223
+ constructor(container: HTMLElement);
224
+ private resize;
225
+ addCircle(id: string, target: DOMRect, color?: string, thickness?: number): void;
226
+ addHighlight(id: string, target: DOMRect, color?: string, thickness?: number): void;
227
+ addArrow(id: string, target: DOMRect, color?: string, thickness?: number): void;
228
+ addText(id: string, target: DOMRect, text: string, color?: string): void;
229
+ addZoomIndicator(id: string, target: DOMRect): void;
230
+ remove(id: string): void;
231
+ clear(): void;
232
+ private startAnimation;
233
+ private stopAnimation;
234
+ private update;
235
+ private render;
236
+ private drawCircle;
237
+ private drawHighlight;
238
+ private drawArrow;
239
+ private drawText;
240
+ private drawZoomIndicator;
241
+ destroy(): void;
242
+ }
243
+
244
+ declare function parseInstruction(text: string): ParsedInstruction;
245
+ declare function parseNaturalLanguageScenario(script: string, options?: {
246
+ name?: string;
247
+ description?: string;
248
+ startUrl?: string;
249
+ }): Scenario;
250
+
251
+ interface UseScenecapReturn {
252
+ /** Ref to attach to the container element you want to record */
253
+ containerRef: React.RefObject<HTMLElement>;
254
+ /** Record a natural language script */
255
+ record: (script: string, options?: {
256
+ name?: string;
257
+ description?: string;
258
+ }) => Promise<Blob | null>;
259
+ /** Record a structured scenario */
260
+ recordScenario: (scenario: Scenario) => Promise<Blob | null>;
261
+ /** Parse a script into a scenario (for previewing actions) */
262
+ parse: (script: string) => Scenario;
263
+ /** Download the last recording */
264
+ download: (filename?: string) => void;
265
+ /** Whether a recording is in progress */
266
+ isRecording: boolean;
267
+ /** Current progress 0-100 */
268
+ progress: number;
269
+ /** Current session info */
270
+ session: RecordingSession | null;
271
+ /** Last error, if any */
272
+ error: string | null;
273
+ /** The last recorded blob */
274
+ blob: Blob | null;
275
+ }
276
+ /**
277
+ * React hook for using Scenecap in Next.js / React projects.
278
+ *
279
+ * @example
280
+ * ```tsx
281
+ * function DemoRecorder() {
282
+ * const { containerRef, record, isRecording, progress, download } = useScenecap();
283
+ *
284
+ * const handleRecord = async () => {
285
+ * await record(`
286
+ * Click the "Get Started" button
287
+ * Type "hello@example.com" in the email input
288
+ * Zoom in on the submit button
289
+ * Circle the success message in green
290
+ * `);
291
+ * download('demo.webm');
292
+ * };
293
+ *
294
+ * return (
295
+ * <div>
296
+ * <button onClick={handleRecord} disabled={isRecording}>
297
+ * {isRecording ? `Recording... ${progress}%` : 'Record Demo'}
298
+ * </button>
299
+ * <div ref={containerRef}>
300
+ * {/* Your app UI here *\/}
301
+ * </div>
302
+ * </div>
303
+ * );
304
+ * }
305
+ * ```
306
+ */
307
+ declare function useScenecap(config?: Partial<ScenecapConfig>): UseScenecapReturn;
308
+
309
+ /**
310
+ * Resolves a CSS selector (including custom :has-text()) to a DOM element.
311
+ * Falls back through multiple strategies to find the best match.
312
+ */
313
+ declare function resolveElement(selector: string, root?: Element): Element | null;
314
+ /**
315
+ * Smoothly scrolls an element into view.
316
+ */
317
+ declare function scrollIntoView(el: Element, smooth?: boolean): Promise<void>;
318
+ /**
319
+ * Simulates a mouse click with visual feedback.
320
+ */
321
+ declare function simulateClick(el: Element): void;
322
+ /**
323
+ * Simulates typing text into an input field.
324
+ */
325
+ declare function simulateType(el: Element, text: string, speed?: number): Promise<void>;
326
+
327
+ /**
328
+ * Virtual cursor that appears in recordings.
329
+ * Renders a smooth macOS-style pointer that animates between targets.
330
+ */
331
+ declare class VirtualCursor {
332
+ private el;
333
+ private x;
334
+ private y;
335
+ private visible;
336
+ constructor(container: HTMLElement);
337
+ show(): void;
338
+ hide(): void;
339
+ /**
340
+ * Smoothly move cursor to a position.
341
+ */
342
+ moveTo(targetX: number, targetY: number, duration?: number): Promise<void>;
343
+ /**
344
+ * Move to center of a DOM element.
345
+ */
346
+ moveToElement(el: Element, duration?: number): Promise<void>;
347
+ /**
348
+ * Click animation at current position.
349
+ */
350
+ clickAnimation(): Promise<void>;
351
+ destroy(): void;
352
+ }
353
+
354
+ /**
355
+ * Create a Scenecap instance with the given config.
356
+ *
357
+ * @example
358
+ * ```ts
359
+ * import { createScenecap } from 'scenecap';
360
+ *
361
+ * const sc = createScenecap({
362
+ * recording: { format: 'webm', fps: 30 },
363
+ * annotations: { circleColor: '#ff0000' },
364
+ * });
365
+ *
366
+ * const blob = await sc.record(`
367
+ * Click on the login button
368
+ * Type "admin" in the username field
369
+ * Zoom in on the password field
370
+ * Circle the submit button
371
+ * `, document.getElementById('app')!);
372
+ * ```
373
+ */
374
+ declare function createScenecap(config?: Partial<ScenecapConfig>): ScenarioRunner;
375
+
376
+ export { type ActionType, AnnotationOverlay, type OverlayAnnotation, type ParsedInstruction, type RecordingOptions, type RecordingSession, type Scenario, type ScenarioAction, ScenarioRunner, type ScenecapCallbacks, type ScenecapConfig, type UseScenecapReturn, VideoRecorder, VirtualCursor, ZoomController, createScenecap, parseInstruction, parseNaturalLanguageScenario, resolveElement, scrollIntoView, simulateClick, simulateType, useScenecap };