# Getting started

> Create a framefields project with one command (or by hand), and verify a first render from the cheapest check to the final MP4.

## 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.

```bash
pnpm dlx framefields init my-film --install   # or: npx framefields init my-film --install
cd my-film && pnpm frames -- 0 30 60          # first frames in output/
```

- **Nothing to answer.** `init` asks 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`, `.gitignore` and `.gitattributes` only 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 generated `src/components/fonts.ts` registers them, and `theme.ts` calls 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](https://framefields.dev/dashboard), copy `.env.example` to `.env` and set `FRAMEFIELDS_API_KEY`. `render.ts` loads `.env` when it exists. See [Generative AI](/docs/guides/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](#4-verify-cheapest-step-first). 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

```bash
npx framefields list              # ready-made components, one line each
npx 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

```bash
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](/docs/guides/rendering/#rendering-without-a-gpu).
- **ES modules.** `package.json` needs `"type": "module"`.

## 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.

```json title="package.json"
{
  "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](/docs/guides/render-service/) installs with exactly that version and lockfile.

```bash
npm install framefields
npm install --save-dev tsx typescript @types/node
```

:::caution
If you see `Cannot find module '.../skia.node'`, the `skia-canvas` script was skipped. Approve it as above, then run `npm rebuild skia-canvas` (or `pnpm rebuild skia-canvas`).
:::

```json title="tsconfig.json"
{
  "compilerOptions": {
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "target": "ES2022",
    "strict": true,
    "skipLibCheck": true,
    "noEmit": true,
    "types": ["node"]
  },
  "include": ["src/**/*"]
}
```

## 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 | video
assets/       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 / .webp
output/       renders (gitignored, never committed)
.gitattributes  LFS rules for assets/
```

```ts title="src/theme.ts"
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") });
}
```

```ts title="src/film.ts"
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;
}
```

```ts title="src/render.ts"
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

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](/docs/guides/preview/)) |
| 5 | `npm run render` | the MP4, plus QA findings ([Rendering](/docs/guides/rendering/)) |

## 5. Store assets with Git LFS

Keep every input file under `assets/` and store it with [Git LFS](https://git-lfs.com). Set it up before adding any media:

```bash
git lfs install
git lfs track "assets/fonts/**" "assets/footage/**" "assets/audio/**" "assets/images/**" \
  "assets/luts/**" "assets/models/**" "assets/lottie/**"
echo "output/" >> .gitignore
git 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`).

:::caution
Until pulled, LFS files are ~130-byte pointers. framefields cannot decode them: fonts fail to register and media renders blank. Ignore `output/`, not `*.mp4`, or your footage disappears from Git too.
:::

Details, including fixing a file committed without LFS: [Assets and Git LFS](/docs/agents/workflow/#assets-and-git-lfs).

## Next

- [Core concepts](/docs/core-concepts/) — how frames, layers and timing fit together.
- [framefields for agents](/docs/agents/) — install the skills so your agent knows all of this.
- [Render on a server](/docs/guides/render-service/) — render the project from git on a GPU worker.
