demo-video-creator

star 42

Record a polished 4K screencast/demo video of Daintree by driving the in-app demo engine (window.electron.demo — animated cursor, spotlight, captions) from a Playwright spec. Use whenever the user wants to make a demo video, screencast, product/marketing clip, or feature walkthrough of Daintree — either from a standard fixture or from a specific existing project folder they point at ("create a video from this folder").

daintreehq By daintreehq schedule Updated 6/7/2026

name: demo-video-creator description: Record a polished 4K screencast/demo video of Daintree by driving the in-app demo engine (window.electron.demo — animated cursor, spotlight, captions) from a Playwright spec. Use whenever the user wants to make a demo video, screencast, product/marketing clip, or feature walkthrough of Daintree — either from a standard fixture or from a specific existing project folder they point at ("create a video from this folder").

Demo Video Creator

This skill produces a real recorded video (.webm master + .mp4) of Daintree in action. It does NOT mock anything — it launches the actual app with the demo engine enabled and choreographs a fake cursor, spotlight, and captions over a real project while MediaRecorder captures the frame to disk.

How it works (architecture)

Two halves:

  1. The in-app demo engine (already built, demo-mode only):
    • src/components/Demo/DemoCursor.tsx — animated cursor: auto-generated Bézier arcs, Fitts's-law timing, ballistic→settle, click ripples. You give it destinations; the realism is automatic.
    • src/components/Demo/DemoOverlay.tsx — spotlight (blur + dim, masked cutout, 300ms fades) and captions (placement presets, size tiers, 200ms fades).
    • src/components/Demo/DemoCaptureBridge.tsxgetDisplayMedia + MediaRecorder → streams chunks to the main process.
    • electron/ipc/handlers/demo.ts + electron/preload.cts — the window.electron.demo API.
  2. The Playwright driver (the "scene"): e2e/screenshots/demo-reel.spec.ts is the canonical template. It boots a project with --demo-mode, then awaits window.electron.demo.* calls wrapped in startCapture/stopCapture.

Read e2e/screenshots/demo-reel.spec.ts first — copy/adapt it for each new video rather than starting from scratch.

Two modes

Mode A — standard fixture. Reuse the marketing fixtures in e2e/helpers/screenshotFixtures.ts (createBrushCmsRepo = worktree dashboard, createSurgeCheckoutRepo = agent at work, createOrbitalSyncRepo = multi-agent, etc.). Each returns { dir, cleanup }. This is the default — no setup, deterministic, no API key needed for the non-agent fixtures.

Mode B — a specific project folder. The user sets up a real project folder ("I created a whole project, now make a video from this folder") and gives you its absolute path. Open THAT path instead of a fixture: skip createBrushCmsRepo() and pass the user's path as the dir to mockOpenDialog. First inspect the folder (its worktrees, files, what's interesting) and choreograph the scene around its actual content — generic captions on unknown content look hollow.

The boot flow is identical for both; only the folder dir differs.

Workflow

  1. Build the e2e bundle (demo mode is stripped from prod but present in the test build): npm run build:e2e
  2. Author the scene — copy demo-reel.spec.ts to e2e/screenshots/<name>.spec.ts (or edit in place for iteration) and write the beats (see API below). Wrap the visible beats in startCapture(...) / stopCapture(), inside a try/finally so capture always finalizes; make each beat best-effort (safe() wrapper) so one missing selector doesn't abort the recording.
  3. Record: npx playwright test e2e/screenshots/<name>.spec.ts --project=screenshots --reporter=line (The screenshots Playwright project has a 30-min timeout. A transient macOS launch flake auto-retries.)
  4. Post-process (output lands in artifacts/demo/, which is gitignored):
    • Fix the WebM duration (MediaRecorder omits it during live muxing): ffmpeg -i in.webm -c copy fixed.webm
    • Optional QuickTime-friendly copy: ffmpeg -i fixed.webm -c:v libx264 -preset slow -crf 18 -pix_fmt yuv420p -movflags +faststart out.mp4
  5. Verify before claiming done — extract frames with ffmpeg -ss <t> -i out.webm -frames:v 1 frame.png and actually look at them (a tiled montage -vf "fps=15,scale=300:-1,tile=7x3" shows fades ramping). Confirm the feature is on screen, the cursor moves, and captions are legible.

The window.electron.demo API (call via page.evaluate)

All calls return promises that resolve when the command's renderer round-trip completes, so await them. The cursor/overlay components must be mounted first — gate on await page.waitForFunction(() => !!window.electron?.demo).

  • Cursor: moveTo(xPct, yPct, durationMs?) (x/y are 0–100 % of viewport), moveToSelector(selector, durationMs?, offsetX?, offsetY?), click() (clicks at the cursor's current spot — move first), drag(fromSel, toSel, durationMs?), scroll(selector), pressKey(key, code?, modifiers?, selector?), type(selector, text, cps?).
  • Terminals: typeInTerminal(selector, text, cps?) types text into an xterm.js terminal panel char-by-char with humanized timing, and sendKeyToTerminal(selector, key) sends a named special key. These route through the PTY (window.electron.terminal.write), unlike type/pressKey which target CodeMirror/HTML inputs and do not work on terminals. The selector resolves to (or to any element inside) a terminal panel's [data-panel-id]. Supported keys: up, down, left, right, enter, tab, escape, backspace, ctrl-c, ctrl-d, ctrl-u, home, end, pageup, pagedown (arrow keys auto-honor the terminal's application-cursor mode). Example: await d.typeInTerminal('[data-panel-id="…"]', 'echo hi'); await d.sendKeyToTerminal('[data-panel-id="…"]', 'enter');.
  • Spotlight: spotlight(selector, padding?) (blur + dim everything else, 300ms fade-in), dismissSpotlight().
  • Captions: annotate(selector, text, placement?, size?, id?) → returns { id }. dismissAnnotation(id?) (omit id = clear all; 200ms fades both ways).
    • placement: element-anchored top|bottom|left|right; viewport screen-bottom (subtitle — the default for narration), screen-top, screen-center, lower-third-left|right, top-left|top-right|bottom-left|bottom-right; cursor above-cursor|below-cursor. For viewport/cursor placements pass "" as the selector.
    • size: sm|md|lg|xl (resolved as % of frame height so it scales 1080p↔4K). Default md; subtitles read well at lg.
  • Timing/util: sleep(ms), waitForSelector(selector, timeoutMs?), waitForIdle(settleMs?, timeoutMs?), pause()/resume(), screenshot(). waitForIdle settles only once all activity channels are simultaneously quiet for settleMs — CSS/WAAPI animations, DOM mutations, xterm terminal output (subscribed via each live terminal's onWriteParsed), and <video> playback (a playing video blocks idle until it pauses/ends). It returns early at timeoutMs regardless. Safe to use right after typeInTerminal to wait for a command's output to drain. The main-process watchdog is command-aware: it derives its budget from each command's own durationMs/timeoutMs (or text length for type/typeInTerminal) plus a buffer, so a long sleep, waitFor*, or type beat above 30s is honored rather than silently timed out. Unbounded commands (click, scroll, pressKey, …) keep a 30s default.
  • Capture: startCapture({ outputPath, fps?, videoBitsPerSecond?, width?, height? }), stopCapture(){ outputPath, frameCount }, getCaptureStatus(). outputPath is a main-process filesystem path; the main process mkdirs its dirname.

Quality / resolution knobs (env, read by the spec)

Defaults give a 4K master at the app's true desktop density (the industry sweet spot — Screen Studio / Stripe / Linear all target logical 1920×1080 @ 2× → 4K). Do NOT zoom the interface by default: zooming reduces the effective layout and hides panels, and setZoomFactor literally magnifies fonts so little fits.

Env Default Effect
DAINTREE_DEMO_RESOLUTION 4k 1080p / 1440p / 4k output
DAINTREE_DEMO_ZOOM 1.0 UI magnification; >1 fits less, <1 fits more. Leave at 1.0 unless asked.
DAINTREE_DEMO_FPS 60 frame rate
DAINTREE_DEMO_BITRATE_MBPS 50 (4k) encode ceiling (VBR — static dark UI stays small)

Scene-authoring lessons (hard-won — follow these)

  • Tell one story. Intro subtitle → 2–4 focused beats → outro subtitle. Don't narrate every pixel.
  • Captions are optional and most users want few of them. When used: keep ≤2 lines, sentence case, and hold them long enough to read — ~15–17 characters/second (a 36-char line ≈ 2.2s minimum on screen).
  • Show the feature within the first ~0.5s — open on the real UI, not a title card.
  • Cursor + click reads as intent. moveToSelector then click() plays the press/ripple and (for worktree cards etc.) performs the real action — capture the resulting state change.
  • Spotlight to focus, then dismiss before moving on. On Daintree's dark theme the blur (not the dim) is what makes the focus read.
  • Reading-friendly pacing: sleep generously between beats; IPC round-trips already add latency, so the recording runs a touch longer than the sum of sleeps.

Gotchas / why things are the way they are

  • Demo mode must reach the renderer. It's gated on the renderer's process.argv; --demo-mode is forwarded into additionalArguments in electron/window/createWindow.ts and ProjectViewManager.ts. Launch via launchApp({ extraArgs: ["--demo-mode"], windowSize, screenshotScale }).
  • getDisplayMedia needs a relaxed Permissions-Policy. display-capture is denied by default; electron/setup/protocols.ts relaxes it to (self) only in demo mode (gated on !app.isPackaged).
  • Page handle goes stale on open. The project view reloads once during hydration. Re-acquire with refreshActiveWindow after opening the folder (the template does this twice) before driving the demo.
  • Capture file is small even at 4K — that's efficient VBR on a static dark UI, not low quality. Judge sharpness from a native-res crop, not the byte count.
  • Animated camera zoom was removed (webFrame.setZoomFactor thrashed xterm's WebGL atlas every frame). Z-axis "camera moves" belong in post (e.g. a CSS transform in Remotion), not at capture time.

Output

Videos go to artifacts/demo/ (gitignored). Commit only the code (engine + spec changes), never the rendered media.

Install via CLI
npx skills add https://github.com/daintreehq/daintree --skill demo-video-creator
Repository Details
star Stars 42
call_split Forks 6
navigation Branch main
article Path SKILL.md
More from Creator