# Skriv film med en AI-agent

MCP-serveren, der giver en kodeagent reglerne for at skrive film, dokumentationen, de verificerede opskrifter og et kontrolværktøj til det, den skriver.

Canonical: https://zxn.studio/da/motion/ai-agents

En film egner sig godt til generering. Dens input er JSON, dens output er en typet React-komponent, og korrekthed handler mest om at følge en håndfuld regler. Men en agent, der overlades til sig selv, skriver film, der er langsomme, flimrer eller renderes anderledes end forhåndsvisningen, af grunde, der ikke har noget at gøre med, hvor god den er til TypeScript.

**Den kan ikke se resultatet.** Alt andet følger af det. Et menneske skriver en sløring, ser på den og skruer ned. En agent skriver den og går videre.

**Den vælger et DOM-element pr. ting.** Bedt om regn, sne, stjerner eller et partikelfelt er standardsvaret en `<div>` pr. partikel med glød. Det er tusind elementer, der får beregnet layout og tegnes på hvert billede, og sløring er den dyreste optegning, der findes. Det ser fint ud på et skærmbillede og tager en time at rendere.

**Den vælger rigtige ure.** `Date.now()`, `setInterval`, `requestAnimationFrame`, en CSS `transition`, `useState`, der akkumulerer på tværs af renderinger, og `timeline.play()`. Det er alle måder, animation normalt virker på nettet, og de er alle forkerte her: En film skal kunne tegne ethvert billede for sig i enhver rækkefølge, fordi det er det, både tidslinjesøgning og rendering gør.

**Den bruger `Math.random()`** — et nyt billede ved hver rendering, så forhåndsvisningen og eksporten afviger.

**Den kopierer projektets opløsning.** ZXN Studio optager i 4K, så en agent, der har læst den omgivende kode, laver en film i 3840×2160. Fire gange så mange pixels giver fire gange så meget optegning på hvert billede for noget, en vært alligevel ville skalere.

Intet af det løses med en længere prompt. Det løses ved at give agenten reglerne, de fungerende eksempler og et kontrolværktøj fra én fælles sandhedskilde via MCP.

## Serveren

`@zxn/motion-mcp` er en Model Context Protocol-server med binærfilen `zxn-motion-mcp`, der leverer tre ting:

- **reglerne for at skrive film**, korte nok til at blive læst ved begyndelsen af hver session;
- **dokumentationen og de verificerede opskrifter** — de samme filer, som dette websted udgiver, og testsættet monterer, ikke en kopi skrevet til agenten. Intet, den får vist, kan være et eksempel, der er holdt op med at virke;
- **et kontrolværktøj**, der typekontrollerer kildekode mod de rigtige deklarationer i `@zxn/motion-core` og leder efter vanerne ovenfor. Intet udføres.

### Værktøjer

| Værktøj | Input | Returnerer |
| --- | --- | --- |
| `authoring_rules` | — | Reglerne nedenfor, ordret. |
| `search_docs` | `query` (streng, mindst 2 tegn), `limit` (1–12, standard 6) | Matchende dokumentationsafsnit og eksempler, de bedste først. Dokumentationstræffere har overskriften `## Title › Section (read_doc topic: topic)` og indeholder op til 1.200 tegn af afsnittet; eksempeltræffere har en overskrift med det id, der skal sendes til `get_example`. |
| `list_docs` | — | Alle dokumentationsemner med titel og resumé. |
| `read_doc` | `topic` | Hele Markdown-indholdet for ét emne. Et ukendt emne returnerer listen over gyldige emner. |
| `list_examples` | — | Alle verificerede eksempler med resumé, teknikker og pakker. |
| `get_example` | `id` | Hele kildekoden til ét eksempel med titel, resumé og sti som en kommentar øverst. |
| `check_film` | `entryFile`, `files` (et map fra sti → kildekode) | Fund eller `No problems found.` (ingen problemer fundet). |

`search_docs` opdeler en side ved dens `##`-overskrifter og vurderer afsnittene, så en træffer peger på det afsnit, der besvarer spørgsmålet, frem for en side, agenten bagefter skal læse. Et afsnit, der matcher alle søgeord, slår et, der nævner ét ord fyrre gange.

`check_film` er værktøjet, der skal kaldes ofte. `entryFile` er filen, der eksporterer `films`; `files` indeholder hver kildefil med dens sti relativt til indgangsfilens mappe som nøgle. Det rapporterer:

- **fejl** ved typefejl fra SDK'ets rigtige deklarationer, ved en import, en film ikke må bruge (det angiver, hvad der *er* tilgængeligt), ved en `entryFile`, der mangler i `files`, og ved en indgangsfil uden `export const films =`;
- **advarsler** om vanerne, hver med en anvisning på, hvad der skal gøres i stedet.

Præcis de vaner, det opdager:

- `Array.from({ length: N })`, hvor N har tre eller flere cifre — hundredvis af elementer eller flere; brug ét `<Canvas2D>`.
- `setInterval(` eller `requestAnimationFrame(` — timere kører efter et ur.
- `Date.now()`, `new Date()` eller `performance.now()` — en film må ikke aflæse det rigtige ur.
- `Math.random()` — brug `seededRandom(seed)`.
- `createNoise2D()`, `createNoise3D()` eller `createNoise4D()` kaldt uden argument — en støjfunktion uden startværdi giver et nyt felt ved hver rendering.
- `.play()`, `.restart()` eller `.resume()` — opret biblioteket pauset, og sæt dets position.
- En CSS `transition:`, `animation:` eller `@keyframes` — styret af browserens ur, ikke billednummeret.
- `useState(` eller `useReducer(` — tilstand, der akkumuleres på tværs af billeder, ødelægger søgning med tidslinjeskyderen.
- En `textShadow`, `boxShadow`, `filter: "blur…"` eller `backdropFilter`, der optræder inden for cirka 1.200 tegn efter en `.map(` eller en `Array.from({ … }, callback)` — sløring på hvert element i en liste.
- Et `width`/`height`-par med mindst fire cifre i hver værdi, hvis produkt overstiger 1920×1080 — større, end filmen behøver at være.
- En `defaultProps`-nøgle uden en tilsvarende post i `controls` — en prop, der kun kan ændres ved at redigere kode.

Reglerne for kodevaner er mønstre i kildekoden med kommentarerne erstattet af blanktegn, så en kommentar som »kald aldrig `Date.now()`« ikke rapporteres som et kald. Det er stadig mønstre: En af dine egne metoder kaldet `play()` bliver markeret, og den advarsel kan ignoreres.

Kun kildekode leveret af agenten rapporteres; bibliotekets egne deklarationer er ikke dens anliggende. Hvis `@zxn/motion-core` ikke kan findes ved siden af serveren eller i den mappe, den blev startet fra, kontrolleres typerne ikke, og resultatet oplyser det i stedet for at rapportere nul typefejl. En pakke, en film må importere, men som ikke er installeret ved siden af kontrolværktøjet, som `lottie-web` i en frisk klon, rapporteres som en advarsel om, at dens kode ikke kunne typekontrolleres, ikke som en fejl i filmen.

### Reglerne, den leverer

`authoring_rules` returnerer dette, og serveren sender det også som sine MCP-instruktioner, så en klient, der læser dem, har det før første værktøjskald.

```text
# Writing a ZXN Motion film

1. A film is a pure function of its props and an integer frame. Read the frame with useTimeline(); never a clock, a timer, state that accumulates, or Math.random(). Frames are requested in any order.
2. Export `const films = [defineFilm({ id, title, width, height, frameRate, frames, component, defaultProps, controls })]`. Author at 1920×1080 and 30 fps unless asked otherwise; a host scales the film.
3. Give every prop a control, so a person can change it without opening the code.
4. Few large elements, not thousands of small ones. Anything particle-like (rain, snow, stars, noise, a field of lines) goes on ONE <Canvas2D draw={(ctx, frame) => ...}>. Blur and shadows are the expensive paint: never inside a loop.
5. Use DOM or SVG for things a person would select: titles, bars, labels, shapes. Give those a stable data-zxn-element-id. Do not give ids to particles.
6. Libraries are seeked, never played: build a GSAP timeline paused and call timeline.seek(seconds); lottie.goToAndStop(frame, true). Seed any noise or random source with seededRandom().
7. Motion that reads well: ease everything (easing.easeOut in, easeIn out); springValue for arrivals with weight; hold long enough to read (about 1 s per 3 words); stagger related items 2–4 frames apart; move things far enough to notice, quickly enough to feel deliberate (a full-screen travel in 0.4–0.8 s).
8. Before proposing source: call search_docs for the technique, get_example for the nearest recipe, then check_film on what you wrote and fix every error and warning.
```

Listen slutter med de pakker, en film må importere: `react`, `react/jsx-runtime`, `@zxn/motion-core`, `@zxn/motion-graphics`, `@zxn/motion-text`, `@zxn/ui`, `lucide-react`, `clsx`, `tailwind-merge`, `class-variance-authority`, `radix-ui`, `d3-scale`, `d3-shape`, `d3-interpolate`, `d3-ease`, `gsap`, `lottie-web`, `simplex-noise` og `culori`.

## Opsætning

Pakken er endnu ikke på npm, så den kører fra en klon. Node 22 eller nyere.

```bash
git clone https://github.com/zinxan/motion-graphics.git
cd motion-graphics
npm install && npm run build
```

Det producerer `packages/mcp/dist/bin.js`. Kør den med en absolut sti — en agents arbejdsmappe er ikke din.

Claude Code:

```bash
claude mcp add zxn-motion -- node /absolute/path/to/motion-graphics/packages/mcp/dist/bin.js
```

Det samme som en versionsstyret `.mcp.json` i roden af det projekt, du laver film i:

```json
{
  "mcpServers": {
    "zxn-motion": {
      "command": "node",
      "args": ["/absolute/path/to/motion-graphics/packages/mcp/dist/bin.js"]
    }
  }
}
```

Cursor, i `.cursor/mcp.json`:

```json
{
  "mcpServers": {
    "zxn-motion": {
      "command": "node",
      "args": ["/absolute/path/to/motion-graphics/packages/mcp/dist/bin.js"]
    }
  }
}
```

Enhver anden stdio-klient skal bruge de samme to oplysninger — kommandoen `node` og ét argument, den absolutte sti til `bin.js`. Serveren taler MCP på stdout og skriver `zxn-motion MCP server ready.` på stderr; en klient, der betragter stderr som en fejl, misforstår den.

Kør serveren fra klonen, så læser den `docs/` og `examples/recipes/` direkte, og en sideredigering slår igennem uden genopbygning. Behold klonens `node_modules`: Det er sådan, `check_film` finder SDK'ets deklarationer og overhovedet kan rapportere typefejl.

## Arbejdsgangen

En agent, der følger denne rækkefølge, skriver som regel en god film i første forsøg.

1. **`authoring_rules`**, én gang pr. session, før noget skrives.
2. **`search_docs`** efter teknikken — »canvas particles« (partikler på canvas), »spring overshoot« (fjederens oversving), »gsap seek« (sæt positionen i GSAP), »tempo beats« (tempo og slag), »controls props« (kontroller og egenskaber). Brede emneord virker bedre end en sætning.
3. **`list_examples`**, derefter **`get_example`** for det nærmeste eksempel. At starte fra en fungerende film er bedre end at starte fra ingenting, og opskrifterne indeholder allerede de beslutninger, reglerne kun beskriver.
4. **Skriv** filmen. Én indgangsfil, der eksporterer `films`, og en kontrol til hver prop.
5. **`check_film`** på præcis det, agenten er ved at foreslå.
6. **Ret** hver fejl og hver advarsel, og kontrollér igen. En advarsel siger altid, hvad der skal gøres i stedet, så der er intet at fortolke.
7. **Foreslå** kildekoden, og angiv, hvilke billeder man skal se på. Et menneske skal stadig se filmen.

En prompt, der er værd at indsætte:

> Skriv en ZXN Motion-film: et titelkort på 6 sekunder til "Field Notes", mørkt, med en linje af små, drivende partikler bag teksten. Brug zxn-motion-værktøjerne — læs reglerne for at skrive film, søg i dokumentationen efter teknikken, start fra det nærmeste eksempel, og kør check_film på din kildekode, indtil det ikke rapporterer nogen problemer, før du viser mig den. Fortæl mig derefter, hvilke billeder jeg skal trække afspilningsmarkøren hen til.

At nævne `check_film` i prompten betyder mere end noget andet i den.

## En regelfil til dit projekt

Sæt dette i din `AGENTS.md` eller `CLAUDE.md`, så det gælder uden at skulle efterspørges.

```markdown
## Motion films

Films in this project are ZXN Motion films: a React component that is a pure
function of its props and an integer frame.

Before writing or editing one, use the `zxn-motion` MCP server:

- `authoring_rules` first, every session.
- `search_docs` for the technique, then `get_example` for the nearest recipe
  from `list_examples`. Start from a working film, not from nothing.
- `check_film` on the source before proposing it. Fix every error and every
  warning, then run it again. Do not show me source that has not passed.

Never use `Date.now()`, `setInterval`, `requestAnimationFrame`, `Math.random()`,
CSS transitions or `useState` in a film. Author at 1920×1080 and 30 fps.
Anything particle-like goes on one `<Canvas2D>`.
```

## Hvad kontrolværktøjet fanger

Dette er den film, en agent uden vejledning skriver, når den bliver bedt om faldende sne. Den kompilerer, og hvert billede er forkert på sin egen måde.

```tsx
import { FullFrame, defineFilm, useTimeline } from "@zxn/motion-core";

type Props = { readonly color: string };

function Snow({ color }: Props) {
  const { frame } = useTimeline();
  const drift = Math.sin(Date.now() / 1000) * 20;
  const flakes = Array.from({ length: 1200 }, (_, index) => ({
    id: index,
    x: Math.random() * 3840,
    y: (Math.random() * 2160 + frame * 7) % 2160,
  }));

  return (
    <FullFrame style={{ background: "#0b1020" }}>
      {flakes.map((flake) => (
        <div
          key={flake.id}
          style={{
            position: "absolute",
            left: flake.x + drift,
            top: flake.y,
            width: 7,
            height: 7,
            borderRadius: "50%",
            background: color,
            textShadow: `0 0 14px ${color}`,
          }}
        />
      ))}
    </FullFrame>
  );
}

export const films = [defineFilm({
  id: "snow",
  title: "Snow",
  width: 3840,
  height: 2160,
  frameRate: 30,
  frames: 300,
  component: Snow,
  defaultProps: { color: "#ffffff" },
  controls: { color: { type: "color", label: "Flake" } },
})];
```

`check_film` på den:

```text
0 error(s), 6 warning(s).

WARNING film.tsx:8  This builds hundreds of elements or more. DOM costs layout and paint per element on every frame: draw a field of many things on one <Canvas2D> instead.
WARNING film.tsx:7  A film must not read the real clock. Use useTimeline().frame or useVirtualTime().seconds.
WARNING film.tsx:10  Math.random() differs on every render outside ZXN Studio's sandbox. Use seededRandom(seed) so a frame always draws the same thing.
WARNING film.tsx:11  Math.random() differs on every render outside ZXN Studio's sandbox. Use seededRandom(seed) so a frame always draws the same thing.
WARNING film.tsx:8  A blur or shadow is applied to every item of a list. Blur is the most expensive thing to paint; put one on a shared parent, or draw the list on a <Canvas2D>.
WARNING film.tsx:38  Larger than 1920×1080. A host scales a film to fit; four times the pixels is four times the paint on every frame. Author at 1920×1080 unless asked otherwise.
```

Seks advarsler, der hver angiver linjen og løsningen. Løsningen er ét `<Canvas2D>`, hvor snefnuggenes positioner kommer fra `seededRandom`, i 1920×1080 — altså [opskriften på Matrix-regn](/da/motion/recipes#matrix-rain) med andre tegn, og derfor kommer `get_example` før skrivningen frem for efter.

## Hvad der endnu mangler

Intet værktøj renderer billeder og giver dem tilbage til agenten som billeder. Alt ovenfor mindsker afstanden mellem det, en agent skriver, og det, der virker; intet af det lukker den første og største afstand, nemlig at agenten stadig ikke kan se sin egen film. Den kan få at vide, at sløring er dyrt; den kan ikke få at vide, at teksten er for lille, at udgangen kommer et slag for sent, eller at farven er grim. Et menneske skal se filmen.

Billeder tilbage til agenten er det næste, der skal bygges. Indtil da: Render opskriften, se den, og sig, hvad der er galt.

## Se også

- [Opskrifter](/da/motion/recipes) — de otte eksempler, `get_example` leverer.
- [Kompositions-API](/da/motion/composition-api) — referencen, dokumentationsværktøjerne søger i.
- [Tænk i billeder](/da/motion/thinking-in-frames) — hvorfor ingen af reglerne ovenfor er til forhandling.
- [Ydeevne](/da/motion/performance) — hvad advarslerne om kodevaner beskytter.

