Skip to content

@frapx/tool-render

tool-* — tool A Node command; the page it renders uses @frapx/tool-capture.

Renders long frame sequences without keeping a browser tab open. frapx-render opens your page in headless Chrome, renders a frame range through @frapx/tool-capture, and writes the PNGs to a folder. An interrupted render resumes where it stopped, and the frames can be encoded with ffmpeg at the end.

Terminal window
pnpm add -D @frapx/tool-render

Needs Node 20+, an installed Google Chrome (or npx playwright install chromium with --browser chromium), and ffmpeg on PATH for --encode (plus ffprobe for --audio).

import { registerRenderTarget } from "@frapx/tool-capture";
registerRenderTarget(fx, {
fps: 30,
duration: 24, // the default range is frames 0..719
resolution: [1920, 1080], // the default size; --resolution overrides it
prepare({ params }) {
fx.setAccumulation({ samples: Number(params.spp ?? 64) });
}
});

prepare({ params, fps, format, resolution }) runs before each render job. Pass () => fx when the page replaces its instance. Without a declared resolution or --resolution, the page renders at its own size, which headless is the 1280 × 720 viewport.

Terminal window
vite build
frapx-render dist --out renders/loop --resolution 3840x2160 --set spp=192 --encode prores
# interrupted? continue with the same settings:
frapx-render dist --out renders/loop --resolution 3840x2160 --set spp=192 --encode prores --resume
# a route of a single-page app:
frapx-render dist --path /music --out renders/music

A missing route without a file extension falls back to dist/index.html, so client-side routes render from the build. For a URL source, put the route in the URL instead. vite preview listens on localhost only, which may be IPv6: use the address it prints, or start it with --host 127.0.0.1.

Option Default
--out <dir> required Output directory.
--path <route> / Route to open in a directory, e.g. /music.
--range <a-b> the page’s duration Inclusive frame range.
--fps <n> the page’s fps
--resolution <WxH> the page’s resolution, else its size (headless: 1280 × 720) e.g. 2560x1440, 2048x1080.
--format <png|png16> png 16-bit needs a float buffer in the page.
--set <key=value> Passed to prepare(); repeatable.
--resume Render only the missing frames; the settings must match.
--encode <prores|h264> ProRes 422 HQ .mov or H.264 .mp4, BT.709.
--audio <file> Muxed into each video (PCM 24-bit / AAC 320k), cut to the range.
--strict-audio Stop when the audio and the page’s duration differ by over a frame.
--browser <chrome|chromium> chrome
--headed Show the window.
--allow-software Allow SwiftShader and other software renderers.
--timeout <seconds> 60 How long to wait for the page to call registerRenderTarget().
-h, --help Print the usage text and exit.
-v, --version Print the version and exit.
  • Truncated frames — each PNG is written to a temporary file and renamed.
  • Mixed settings — frapx-render.json in the output folder records the source, route, fps, format, resolution and params; --resume refuses when they differ.
  • Crashes and lost contexts — the page is opened again and the render continues; the frame written right before a lost context is redone. Three failures in a row without progress stop it.
  • Hot reloads — a reload or navigation mid-render stops it, because later frames would come from different code. Render a production build.
  • Mismatched audio — with --audio, the file’s length is compared with the page’s duration before rendering. More than a frame apart warns, or stops with --strict-audio.
  • Silent software rendering — it prints the WebGL renderer and stops on SwiftShader or llvmpipe unless you pass --allow-software.

The CLI is a thin wrapper around render(options, { signal, onEvent }), exported for scripts. options mirrors the flags, onEvent reports progress, and it resolves with the frames rendered and any videos encoded. parseCliArgs(argv) parses frapx-render arguments into those options and throws UsageError for invalid ones.

Full reference: README on GitHub.