The quick way: framefields init
Section titled “The quick way: framefields init”One command sets up everything this page describes: package.json (with the install-script approvals below, scripts, dependencies and packageManager), tsconfig.json, src/theme.ts, film.ts and render.ts, assets/ with Git LFS rules, .gitignore, and the Inter and JetBrains Mono fonts.
pnpm dlx framefields init my-film --install # or: npx framefields init my-film --installcd my-film && pnpm frames -- 0 30 60 # first frames in output/- Nothing to answer.
initasks no questions, so it runs the same in CI and on remote servers. - Existing projects are safe. Run it inside one to add framefields there. Files you have are left alone, and
package.json,.gitignoreand.gitattributesonly gain what they lack. A"type": "commonjs"package is refused, because framefields needs ES modules. - Fonts are pinned. Inter and JetBrains Mono are downloaded from Google Fonts, checked by SHA-256 and saved in
assets/fonts/with their OFL licenses. The generatedsrc/components/fonts.tsregisters them, andtheme.tscalls it. The npm package ships no fonts. - The AI gateway is optional. To generate images, video, music, voice and 3D from code, create a key at framefields.dev/dashboard, copy
.env.exampleto.envand setFRAMEFIELDS_API_KEY.render.tsloads.envwhen it exists. See Generative AI.
| Option | Effect |
|---|---|
--install |
Install dependencies afterwards |
--pm npm|pnpm|yarn|bun |
Package manager (default: the one running the command) |
--emoji |
Also install Noto Color Emoji, so emoji in text draw in color |
--no-fonts |
Skip Inter and JetBrains Mono |
--no-git |
Don’t run git init |
--name <name> |
Package name (default: the folder’s name) |
Then jump to 4. Verify. The steps below set up the same project by hand, for when you want to see each piece or merge into an unusual setup.
More from the CLI
Section titled “More from the CLI”npx framefields list # ready-made components, one line eachnpx framefields add chat-thread # copies a component's source into src/components/npx framefields fonts add emoji # fonts on their own: emoji, sans (Inter), mono (JetBrains Mono)Components that need a font bring it along: chat-thread and captions install the emoji font, terminal and code-block JetBrains Mono. Text with emoji but no emoji font draws nothing, so comp.lint() reports missing_emoji_font and the first render prints how to fix it.
1. Check the machine
Section titled “1. Check the machine”node --version # must be v22 or later- A GPU, or a CPU renderer. Rendering uses WebGPU via Dawn: Metal on macOS (every Mac has a Metal GPU), Vulkan on Linux, Direct3D 12 or Vulkan on Windows. Without a GPU it renders on the CPU, about 12× slower: on Linux install Mesa lavapipe (
apt-get install -y libvulkan1 mesa-vulkan-drivers); Windows has WARP built in. See Rendering without a GPU. - ES modules.
package.jsonneeds"type": "module".
2. Write package.json, then install
Section titled “2. Write package.json, then install”framefields needs exactly one dependency install script: skia-canvas downloads its native binary. npm 11, pnpm 10 and bun block install scripts unless approved, so approve that one and deny the rest before installing.
{ "name": "my-film", "private": true, "type": "module", "allowScripts": { "skia-canvas": true, "sharp": false, "webgpu": false, "onnxruntime-node": false, "node-av": false, "esbuild": false, "protobufjs": false }, "pnpm": { "onlyBuiltDependencies": ["skia-canvas"], "ignoredBuiltDependencies": ["sharp", "webgpu", "onnxruntime-node", "node-av", "esbuild", "protobufjs"] }, "trustedDependencies": ["skia-canvas"], "scripts": { "typecheck": "tsc --noEmit -p .", "frames": "tsx src/render.ts frames", "grid": "tsx src/render.ts grid", "preview": "tsx src/render.ts preview", "render": "tsx src/render.ts" }}allowScripts is read by npm, pnpm by pnpm, trustedDependencies by bun. Yarn needs none of them. Deny the rest: they fetch an ffmpeg CLI and CUDA libraries framefields doesn’t use, or do nothing it needs.
Add "packageManager" with the manager and version you install with (e.g. "pnpm@10.13.1"; pnpm --version prints it), and commit the lockfile it writes. A render worker installs with exactly that version and lockfile.
npm install framefieldsnpm install --save-dev tsx typescript @types/node{ "compilerOptions": { "module": "NodeNext", "moduleResolution": "NodeNext", "target": "ES2022", "strict": true, "skipLibCheck": true, "noEmit": true, "types": ["node"] }, "include": ["src/**/*"]}3. Scaffold
Section titled “3. Scaffold”Keep the composition separate from the script that renders it, so tests, preview and export all build the same film.
src/ theme.ts sizes, fps, colors, fonts — the only place for magic numbers film.ts buildFilm(): returns the Composition render.ts frames | grid | preview | videoassets/ every input file, stored with Git LFS fonts/ .ttf / .otf (text needs a real font file) footage/ .mp4 / .mov / .webm audio/ .mp3 / .wav images/ .png / .jpg / .webpoutput/ renders (gitignored, never committed).gitattributes LFS rules for assets/import path from "node:path";import { fileURLToPath } from "node:url";import { FontManager } from "framefields";
const HERE = path.dirname(fileURLToPath(import.meta.url));
export const W = 1920;export const H = 1080;export const FPS = 30;export const INK = "#0e0d0c";export const PAPER = "#eee9e0";export const SANS = "Inter";
export async function registerFonts(): Promise<void> { // Resolve from the module URL so the browser preview finds the file too. await FontManager.register({ family: SANS, source: path.resolve(HERE, "../assets/fonts/Inter.ttf") });}import { Composition, Layer, LayerAnimation } from "framefields";import { FPS, H, INK, PAPER, registerFonts, SANS, W } from "./theme.js";
const DURATION = 3 * FPS; // frames
export async function buildFilm(): Promise<Composition> { await registerFonts(); // before any text layer is built
const film = new Composition({ width: W, height: H, fps: FPS, durationMs: (DURATION / FPS) * 1000, backgroundColor: INK, });
film.add( Layer.text("Hello, framefields", { id: "title", position: "absolute", x: 0, y: H / 2 - 60, width: W, height: 120, fontFamily: SANS, fontSize: 96, fontWeight: 700, fill: PAPER, align: "center", }).animate( LayerAnimation.create() .fadeIn(0, 20, "power2.out") .fromTo("y", H / 2 - 20, H / 2 - 60, { start: 0, end: 30, ease: "expo.out" }), ), );
return film;}import fs from "node:fs/promises";import path from "node:path";import { HeadlessMediaRenderer, startPreview } from "framefields";import { buildFilm } from "./film.js";
const OUT = path.resolve(import.meta.dirname, "../output");const [mode = "video", ...args] = process.argv.slice(2);
if (mode === "preview") { const session = await startPreview({ entry: new URL("./film.ts", import.meta.url), export: "buildFilm" }); console.log(`Preview at ${session.url}`); await session.closed; process.exit(0);}
const film = await buildFilm();await fs.mkdir(OUT, { recursive: true });
if (mode === "frames") { const renderer = new HeadlessMediaRenderer(); for (const frame of args.map(Number)) { const png = await film.renderFrame({ frame, renderer }); await fs.writeFile(path.join(OUT, `f${String(frame).padStart(4, "0")}.png`), png); }} else if (mode === "grid") { const png = await film.renderFrameGrid({ frames: args.length ? args.map(Number) : [0, 10, 20, 30, 45, 60, 75, 89], columns: 4, cellWidth: 480, }); await fs.writeFile(path.join(OUT, "grid.png"), png);} else { const result = await film.renderVideo({ outputPath: path.join(OUT, "output.mp4"), quality: "high", qa: true }); console.log(`Exported ${result.filePath}`);}
// Native GPU and encoder threads keep Node alive: always exit explicitly.process.exit(0);4. Verify, cheapest step first
Section titled “4. Verify, cheapest step first”Each step catches in seconds what a full render takes minutes to show.
| Step | Command | Look for |
|---|---|---|
| 1 | npm run typecheck |
type errors |
| 2 | npm run frames -- 0 30 60 |
PNGs in output/; a black frame usually means a missing font, an off-canvas layer, or opacity stuck at 0 |
| 3 | npm run grid |
smooth motion across the contact sheet |
| 4 | npm run preview |
a person watches it with sound (Live preview) |
| 5 | npm run render |
the MP4, plus QA findings (Rendering) |
5. Store assets with Git LFS
Section titled “5. Store assets with Git LFS”Keep every input file under assets/ and store it with Git LFS. Set it up before adding any media:
git lfs installgit lfs track "assets/fonts/**" "assets/footage/**" "assets/audio/**" "assets/images/**" \ "assets/luts/**" "assets/models/**" "assets/lottie/**"echo "output/" >> .gitignoregit add .gitattributes .gitignore && git commit -m "Track assets with Git LFS"Push: git add assets/…, commit and git push as usual; the media upload with the push. Run git lfs ls-files before committing to confirm each new file is listed.
Pull: in a fresh clone, container or CI runner, run git lfs pull before rendering (in GitHub Actions, actions/checkout with lfs: true).
Details, including fixing a file committed without LFS: Assets and Git LFS.
- Core concepts — how frames, layers and timing fit together.
- framefields for agents — install the skills so your agent knows all of this.
- Render on a server — render the project from git on a GPU worker.