# Live preview

> Serve a localhost WebGPU player that runs the composition itself, with sound, scrubbing, deep links, scripting and notes pinned on the picture.

`startPreview` serves a page that loads your composition's module and renders it live with WebGPU in the browser — not a video stream. The server only hands over the bundle, your assets, and the soundtrack mixed by the export engine.

```ts title="src/render.ts (preview mode)"
import { startPreview } from "framefields";

const session = await startPreview(
  { entry: new URL("./film.ts", import.meta.url), export: "buildFilm" },
  { title: "Launch film" },
);
console.log(`Preview at ${session.url}`);
await session.closed; // "idle" | "replaced" | "closed"
process.exit(0);
```

`export` names a function returning the `Composition` (may be async) or a `Composition` itself.

## Options

| Option | Default | Notes |
| --- | --- | --- |
| `port` | derived from the project directory | one stable URL per project; a new run replaces the old server and open tabs reload in place |
| `host` | `127.0.0.1` | never leaves the machine |
| `open` | `false` | open the system browser; otherwise show `session.url` where you are |
| `title` | — | page header |
| `audio` | `true` | mix and play the soundtrack |
| `idleCloseMs` | `5000` | stop after the last tab closes; `false` keeps serving until `close()` |
| `root` | project of the entry | extra folders the page may load assets from |
| `log` | stderr | where page errors and lifecycle lines go; `false` silences |
| `notesDir` | `.framefields/preview-notes` | `false` turns notes off |

## Assets must resolve in the browser

The browser runs `film.ts` itself. Build asset paths from the module URL, not `process.cwd()`:

```ts
import path from "node:path";
import { fileURLToPath } from "node:url";

const HERE = path.dirname(fileURLToPath(import.meta.url));
const FONTS = path.resolve(HERE, "../assets/fonts");
```

`node:path`, `fileURLToPath` and synchronous `fs` reads work through shims; writes are skipped.

## For agents

- Run the preview as a **background** command and read its output: it reports when the page opened, each distinct page error (build failure, a frame that threw, missing font, no WebGPU), and why it stopped.
- After editing, run it again — same URL, and the open tab reloads at the same moment.
- Deep link: `#t=12.5` (seconds) or `#frame=750`.
- Script it: `window.framefieldsPreview` has `seek(seconds)`, `seekFrame(n)`, `play()`, `pause()`, `notes()`, `state()`. `seek` resolves once the frame is drawn — screenshot after it. `<html data-preview="ready">` marks the first drawn frame.

## Notes pinned on the picture

Paused, a person clicks a spot (or drags an area) and writes what should change. Each note is printed in the preview output and saved:

```
.framefields/preview-notes/
  notes.json        [{ id, frame, time, x, y, w?, h?, text, done }]
  note-<id>.jpg     the frame with the spot marked
```

To act on notes: read `notes.json`, open each open note's picture, find the layer from the moment and the spot, edit, rerun the preview. Set `"done": true` yourself or let the person tick them off.

## Playback

Play runs in real time on the soundtrack's clock and skips frames a slow device can't draw; the page shows the frame rate it reaches. Paused, it draws the exact frame. Requires WebGPU (current Chrome, Edge or Safari).
