Skip to content

Capturing demo frames

Marketing helixui without GIFs is hard. Recording GIFs manually drifts — a UI tweak invalidates yesterday’s recording. So we automate.

The script

tests/demos.spec.ts defines three demo flows:

TestWhat it captures
demo: three-brandsDNA Lab cycling through wildtype → studio → botanic → solstice → noir.
demo: markdown-to-pptxMarkdown studio, hovering and clicking the .pptx export.
demo: breed-a-themeDNA Lab rolling 4 times and clicking Share.

Each test screenshots after every scripted interaction. Frames land in tests/__demos__/<demo>/frame-NNN.png.

Running

Terminal window
# 1) Start the site somewhere reachable.
pnpm dev:site # or pnpm preview:site after build
# 2) In another terminal, run the demo capture.
pnpm demos

You’ll see ~12 PNGs per demo. They’re 1280×800 by default.

Turning frames into GIFs / MP4s

Frames are just PNGs — assemble them however you like.

Terminal window
ffmpeg -framerate 12 -i tests/__demos__/three-brands/frame-%03d.png \
-vf "fps=12,scale=1280:-1:flags=lanczos" \
apps/site/public/demos/three-brands.gif
# Or video for higher-fidelity Twitter cards:
ffmpeg -framerate 12 -i tests/__demos__/three-brands/frame-%03d.png \
-c:v libx264 -pix_fmt yuv420p -movflags +faststart \
apps/site/public/demos/three-brands.mp4

Gifski produces smaller / sharper GIFs:

Terminal window
gifski --fps 12 --width 1280 tests/__demos__/three-brands/*.png \
-o apps/site/public/demos/three-brands.gif

CI integration

Demo frames are not committed to git — they’re build artifacts. A separate workflow (.github/workflows/demos.yml, coming next sprint) runs the capture on release tags and uploads to the release’s artifact bucket. The README references the latest frame via a stable URL.

Adding a new demo

  1. Add a test('demo: <name>') block to tests/demos.spec.ts.
  2. Use the shoot(page, dir, index) helper for each frame.
  3. Run pnpm demos. Verify the frames are clean.
  4. Pipe through ffmpeg / Gifski; commit the output (the GIF / MP4) to apps/site/public/demos/.

Tips

  • Avoid animations during shots. Pass animations: 'disabled' to page.screenshot if the animation timing makes frames look weird.
  • Use await page.waitForTimeout(300) between interactions so state has time to land.
  • Don’t capture the cursor. Playwright doesn’t render a cursor by default. If you need one for a tutorial GIF, draw it in post or use the chromiumSandboxedShowCursor flag.