# Brug af pakker

Hvilke biblioteker en film må importere, og hvordan du styrer d3, GSAP, simplex-noise og culori ud fra billednummeret i stedet for et ur.

Canonical: https://zxn.studio/da/motion/using-packages

En film er en ren funktion af et heltalligt billednummer. Billeder efterspørges i vilkårlig rækkefølge: En afspiller søger baglæns, en forhåndsvisning hopper til midten, og en eksport går fremad, men kan prøve et billede igen. Den ene kendsgerning afgør, hvordan et tredjepartsbibliotek må bruges.

**Et biblioteks position sættes; det afspilles aldrig. Alt tilfældigt får en fast startværdi.**

Hvis et bibliotek stiller en position til rådighed, du kan sætte — en tidslinje med `seek`, en skala, du kan kalde, en støjfunktion, du kan aflæse ved et koordinat — passer det perfekt til en film. Hvis det kun kan køre sig selv fremad på det rigtige ur, kan det slet ikke bruges.

## Hvad en film må importere

Kontrolværktøjet i `packages/mcp/src/check.ts` har listen i `allowedPackages`. Alt andet er en fejl, ikke en advarsel:

| Pakke | Formål |
| --- | --- |
| `react`, `react/jsx-runtime` | Selve træet. |
| `@zxn/motion-core` | `defineFilm`, `useTimeline`, `Cue`, `mapRange`, `Canvas2D` og resten. |
| `@zxn/motion-graphics` | Deklarative diagrammer og fremhævelser, der er deterministiske for hvert billede. |
| `@zxn/motion-text` | Serialiserbare 2D-tekststile. |
| `@zxn/ui`, `lucide-react`, `clsx`, `tailwind-merge`, `class-variance-authority`, `radix-ui` | Dele til brugergrænseflader i film, der viser et produkt. Se [Film om brugergrænseflader](/da/motion/interface-films). |
| `d3-scale`, `d3-shape`, `d3-interpolate`, `d3-ease` | Ren matematik: værdier til pixels, punkter til stidata, værdi til værdi og fremdrift til fremdrift. |
| `gsap` | Koreografi, oprettet pauset og med positionen sat eksplicit. |
| `lottie-web` | En designers animation, sat til et præcist billede. |
| `simplex-noise` | Glatte generative felter med fast startværdi. |
| `culori` | Farvekonvertering og interpolation i perceptuelle farverum. |

I ZXN Studio følger de med appen: Filmen importerer dem, og de er der. I dit eget projekt installerer du dem selv i den version, din låsefil angiver. Kontrollér de installerede typer, før du støtter dig til et API; denne side bruger kun det, de verificerede eksempler bruger.

## d3-scale og d3-interpolate: rene funktioner passer til en film

d3 er to forskellige biblioteker under ét navn. Den del, der håndterer selektioner og overgange, kører på `requestAnimationFrame` og er ubrugelig her. Matematikdelen — skalaer, interpolatorer og formgeneratorer — tager et tal og returnerer et tal uden tilstand og uden ur. Det er præcis, hvad en film er.

@video(bar-chart-race)

`examples/recipes/bar-chart-race.tsx` lader billednummeret bestemme og d3 udføre regnearbejdet. Billednummeret vælger en position mellem to øjebliksbilleder af dataene; skalaerne omsætter værdier til pixels.

```tsx
function BarChartRace({ title, unit }: Props) {
  const { frame } = useTimeline();
  const position = Math.min(snapshots.length - 1, frame / framesPerStep);
  const index = Math.min(snapshots.length - 2, Math.floor(position));
  // Ease inside each step so the bars settle on every year instead of sliding through it.
  const within = easing.easeInOut(Math.min(1, position - index));
  const from = snapshots[index]!;
  const to = snapshots[index + 1]!;
```

Der er ingen `.transition()` noget sted i filen, og det må der ikke være. En d3-overgang ville starte, når komponenten blev monteret, og køre efter faktisk klokketid, så første billede i en eksport ville fange den midt i bevægelsen, mens en baglæns flytning af afspilningsmarkøren slet ikke ville fange den.

Den detalje, der gør et søjlediagramræs læseligt frem for flimrende, er, at **placeringen interpoleres såvel som værdien**. Hvis en søjle hoppede en række i det øjeblik, den overhalede naboen, ville øjet miste den.

```tsx
  const current = names.map((name) => ({ name, value: interpolateNumber(from.values[name], to.values[name])(within) }));
  // Rank is interpolated too, so a bar glides past its neighbour rather than jumping a row.
  const rankIn = (values: Record<Name, number>, name: Name) => [...names].sort((a, b) => values[b] - values[a]).indexOf(name);
  const x = scaleLinear().domain([0, Math.max(...current.map((bar) => bar.value)) * 1.08]).range([0, chart.width]);
  const y = scaleBand<number>().domain(names.map((_, row) => row)).range([0, chart.height]).padding(0.22);
```

`interpolateNumber(a, b)` returnerer en funktion; billednummeret leverer dens argument. Intet bevares mellem kald, så billede 80 koster det samme, uanset om det følger efter billede 79 eller billede 200.

Hver søjle er bevidst sit eget element med et stabilt id:

```tsx
          <div key={name} data-zxn-element-id={`bar-${name}`} data-zxn-name={`${name} bar`} style={{ position: "absolute", left: 0, top, height: y.bandwidth(), width: "100%", zIndex: after < before ? 2 : 1 }}>
```

Et diagram indeholder et dusin ting, og et menneske vil gerne kunne pege på en af dem. Med et id på hver søjle kan en vært som ZXN Studios egenskabspanel vælge og sætte nøglebilleder på en enkelt søjle frem for hele diagrammet. Se [Kontroller og egenskaber](/da/motion/controls-and-props) om attributternes betydning, og hvor de ikke skal sættes.

Fem søjler er fem elementer, hvilket er ingenting. Et felt af tusind partikler er ikke: Tegn det i stedet på ét `<Canvas2D>`. [Ydeevne](/da/motion/performance) viser regnestykket.

## GSAP: opret pauset, sæt positionen på hvert billede

En GSAP-tidslinje er den behageligste måde at sige »dette, så dét, med en femtedel sekunds overlap«. Den er også som standard noget, der afspiller sig selv. Løsningen er hele teknikken: Opret den **pauset**, og sæt dens position ud fra billednummeret.

@video(gsap-title)

```tsx
  useLayoutEffect(() => {
    const scope = gsap.context(() => {
      timeline.current = gsap.timeline({ paused: true, defaults: { ease: "power3.out" } })
        .from(".bar", { scaleX: 0, transformOrigin: "0% 50%", duration: 0.6 })
        .from(".word", { yPercent: 110, opacity: 0, duration: 0.7, stagger: 0.08 }, "-=0.25")
        .from(".sub", { opacity: 0, x: -30, duration: 0.5 }, "-=0.3")
        .to(".bar", { scaleX: 0, transformOrigin: "100% 50%", duration: 0.5, ease: "power3.in" }, 3.4)
        .to(".word, .sub", { opacity: 0, y: -24, duration: 0.4, stagger: 0.04, ease: "power2.in" }, 3.4);
    }, root);
    return () => scope.revert();
  }, [line1, line2]);

  // After the build effect on the first frame, and on every frame after it.
  useLayoutEffect(() => { timeline.current?.seek(seconds, false); });
```

Fire ting gør arbejdet her.

`gsap.context(…, root)` afgrænser hver selektor til denne films undertræ, så `.word` ikke kan nå ind i en anden film monteret på samme side. `scope.revert()` i oprydningen sætter hvert element tilbage, som det var; uden det ville en genopbygning stable animationer oven på delvist anvendte inline-stile.

Oprettelseseffekten afhænger af teksten, ikke af billednummeret. Den kører én gang og ved ændring af en prop, aldrig pr. billede — at genopbygge en tidslinje tres gange i sekundet ville både være langsomt og meningsløst.

Effekten, der sætter positionen, har **intet afhængighedsarray**, så den kører efter hver opdatering, også den første. `seconds` kommer fra `useVirtualTime()`, som er `frame / frameRate` og intet andet. `seek(seconds, false)` får GSAP til at beregne hele tilstanden for det øjeblik fra bunden, og derfor kan billeder efterspørges i enhver rækkefølge. En layout-effekt er det rette sted: Den kører efter Reacts opdatering og før værtens optagelse af billednummeret, så pixels tilhører det efterspurgte billede.

Samme opbygning passer til alt, hvis position kan sættes. Lottie er det almindelige eksempel:

```tsx
import { useLayoutEffect, useRef } from "react";
import type { AnimationItem } from "lottie-web";
import { useVirtualTime } from "@zxn/motion-core";

export function useSeekedLottie(animation: AnimationItem | null): void {
  const { frame } = useVirtualTime();
  useLayoutEffect(() => { animation?.goToAndStop(frame, true); });
}
```

Det andet argument til `goToAndStop` betyder »dette tal er et billede, ikke et millisekund«. Der er ingen Lottie-opskrift i `examples/`; betragt dette som mønstret, ikke en verificeret film.

Alt, der kun afspiller, giver en advarsel fra kontrolværktøjet. Sådan ser det ud, når det gøres forkert:

```tsx
import { useLayoutEffect, useRef } from "react";
import gsap from "gsap";
import { FullFrame, defineFilm } from "@zxn/motion-core";

type Props = { readonly label: string };

function PlayedTitle({ label }: Props) {
  const root = useRef<HTMLDivElement>(null);

  useLayoutEffect(() => {
    // Wrong: this runs on the browser's clock, so it is a different picture on
    // every render and nothing at all on a scrub backwards.
    const timeline = gsap.timeline().from(".word", { opacity: 0, y: 40, duration: 0.8 });
    timeline.play();
    return () => { timeline.kill(); };
  }, []);

  return (
    <FullFrame style={{ background: "#0b0c0e", alignItems: "center", justifyContent: "center" }}>
      <div ref={root} className="word" style={{ color: "#f3efe7", fontSize: 140, fontWeight: 900 }}>{label}</div>
    </FullFrame>
  );
}

export const films = [defineFilm({
  id: "played-title",
  title: "Played title",
  width: 1920,
  height: 1080,
  frameRate: 30,
  frames: 90,
  component: PlayedTitle,
  defaultProps: { label: "Nope" },
  controls: { label: { type: "text", label: "Label" } },
})];
```

## simplex-noise: giv det en startværdi, og opret det én gang

`createNoise3D()` uden argument henter sin permutationstabel fra `Math.random()`. Opret den to gange, og du har to forskellige felter — hvilket betyder en forskellig film i forhåndsvisningen, når du trækker afspilningsmarkøren, og i eksporten. Giv den i stedet en deterministisk kilde.

@video(noise-field)

```tsx
let draws = 0;
const noise = createNoise3D(() => seededRandom(`noise-field-${draws++}`));
```

`seededRandom(seed)` fra `@zxn/motion-core` returnerer det samme tal for samme startværdi for altid. Tælleren ændrer startværdien løbende, så tabellen fyldes med forskellige værdier, og fordi det hele ligger på modulniveau, oprettes det **én gang**, når modulet indlæses, ikke pr. rendering. Genopbygning inde i komponenten ville bruge samme tæller fra et andet startpunkt som startværdi, og feltet ville forskyde sig.

Tid er den tredje akse, og derfor driver feltet i stedet for at hoppe:

```tsx
      const angle = noise(x / 520, y / 520, seconds * drift) * Math.PI * 2;
```

Nærliggende billeder er nærliggende punkter i samme glatte rumfang, så billedet bevæger sig kontinuerligt, uden at noget videreføres fra sidste billede. Divider koordinaterne for at sætte strukturernes størrelse — større divisor, bredere former — og gang den tredje akse med en hastighed. Sådan et felt af linjer hører hjemme på et `<Canvas2D>`; se [Tegn på canvas](/da/motion/canvas).

## culori: interpoler farver i OKLCH

Blanding af to mættede farver i RGB fører midtpunktet gennem gråt. Kanalerne er ikke perceptuelle, så lysheden falder i midten, og farvetonen driver. OKLCH er bygget, så en ret linje mellem to farver ser ud som en ret linje.

@video(beat-pulse)

```tsx
  const blend = interpolate([from, to], "oklch");
```

```tsx
  const background = formatHex(blend(frame / (film.frames - 1))) ?? from;
```

`interpolate(colours, mode)` returnerer en funktion fra 0–1 til et farveobjekt; `formatHex` omsætter objektet til en streng, DOM'en forstår. Den returnerer `undefined` for en farve, den ikke kan udtrykke, så `?? from` er ikke pynt. Fremdriftsargumentet udledes af billednummeret, så blandingen er endnu et rent opslag.

## Hvad der ikke kan bruges, og hvorfor

Kontrolværktøjet advarer om hver af disse, og hver advarsel angiver, hvad du skal gøre i stedet.

- **Alt, der kun afspiller efter et rigtigt ur.** `setInterval`, `requestAnimationFrame`, en d3-overgang, en Web Animation, der kører, eller en GSAP-animation, der ikke er pauset. Der er intet billede at knytte resultatet til.
- **CSS `transition` og `animation`.** Samme problem ét lag længere nede: Browseren interpolerer efter sit eget ur, og rendereren tager det, der tilfældigvis er på skærmen. Beregn værdien ud fra billednummeret, og sæt den direkte.
- **Alt, der aflæser faktisk klokketid.** `Date.now()`, `new Date()`, `performance.now()`. Brug `useTimeline().frame` eller `useVirtualTime().seconds`. En urskive er en film om tid, ikke en film, der aflæser tiden — `elapsed-clock.tsx` tager sine timer som props.
- **Alt, der kræver netværket.** `fetch`, et analyse-SDK, en skrifttype eller kortflise indlæst ved kørsel, et ikonsæt hentet fra et CDN. En rendering skal producere billede 4.000 lige så pålideligt som billede 1 på en maskine uden netværk og uden cache. Data hører hjemme i props; se [Kontroller og egenskaber](/da/motion/controls-and-props).
- **Nodes indbyggede moduler.** `node:fs`, `node:path`, `node:crypto` og lignende. En film kører på en browserflade i afspilleren, editoren og rendererens skjulte vindue. CLI'et håndterer filsystemet gennem `--props` og `--footage`.
- **Alt, der gemmer tilstand mellem billeder.** `useState`, `useReducer`, en tæller på modulniveau, der stiger pr. rendering, en fysikmotor ført frem af `world.step()` eller et partikelarray, der ændres på stedet. Billeder efterspørges uden for rækkefølge, så »det foregående billede« findes ikke som begreb. Alt, der ligner simulering, skal bruge en lukket formel — derfor beregner `springValue` fjederens position ved et billede i stedet for at simulere den trinvis.
- **`Math.random()`.** Forskelligt ved hver rendering. Brug i stedet `seededRandom(seed)` med startværdi udledt af det, der gør elementet unikt.

Reglen bag dem alle er den samme: Intet må afhænge af, hvordan billednummeret blev nået.

## Kombiner to biblioteker

Intet forhindrer en film i at bruge flere af dem samtidigt, så længe hvert bruges som en funktion. Her tegner d3-shape stien, d3-scale placerer den, og stregen afsløres ud fra billednummeret.

```tsx
import { scaleLinear } from "d3-scale";
import { curveCatmullRom, line } from "d3-shape";
import { FullFrame, defineFilm, easing, mapRange, useTimeline } from "@zxn/motion-core";

/*
 * A line chart that draws itself.
 *
 * d3-shape turns points into path data and d3-scale turns values into pixels.
 * Neither knows anything about time; the frame does the animating. The stroke
 * is revealed with pathLength={1}, which normalises the path to one unit, so
 * the reveal never has to measure the DOM -- measuring is not available before
 * the first paint, and frame zero of an export is a first paint.
 */

type Props = { readonly title: string; readonly accent: string };

const readings: number[] = [4, 9, 7, 15, 12, 22, 19, 29, 35, 32, 44, 51];
const plot = { left: 220, top: 280, width: 1480, height: 560 };

function DrawnLine({ title, accent }: Props) {
  const { frame } = useTimeline();
  const x = scaleLinear().domain([0, readings.length - 1]).range([plot.left, plot.left + plot.width]);
  const y = scaleLinear().domain([0, Math.max(...readings) * 1.1]).range([plot.top + plot.height, plot.top]);
  const path = line<number>().x((_, index) => x(index)).y((value) => y(value)).curve(curveCatmullRom.alpha(0.6))(readings) ?? "";

  const reveal = mapRange(frame, [12, 96], [0, 1], { clamp: true, ease: easing.easeInOut });
  const fade = mapRange(frame, [0, 14], [0, 1], { clamp: true, ease: easing.easeOut });
  // Which reading the head has reached, so the dots land as the line passes them.
  const reached = reveal * (readings.length - 1);

  return (
    <FullFrame style={{ background: "#0f1117", color: "#f3efe7", fontFamily: "Inter, system-ui, sans-serif" }}>
      <div data-zxn-element-id="title" style={{ position: "absolute", left: plot.left, top: 130, fontSize: 68, fontWeight: 800, opacity: fade }}>{title}</div>
      <svg width={1920} height={1080} style={{ position: "absolute", inset: 0 }}>
        {[0, 0.25, 0.5, 0.75, 1].map((step) => (
          <line key={step} x1={plot.left} x2={plot.left + plot.width} y1={plot.top + plot.height * step} y2={plot.top + plot.height * step}
            stroke="rgba(255,255,255,.10)" strokeWidth={2} />
        ))}
        <path data-zxn-element-id="trend" data-zxn-name="Trend line" d={path} fill="none" stroke={accent} strokeWidth={10}
          strokeLinecap="round" strokeLinejoin="round" pathLength={1} strokeDasharray={1} strokeDashoffset={1 - reveal} />
        {readings.map((value, index) => (
          <circle key={index} cx={x(index)} cy={y(value)} r={12 * mapRange(reached, [index - 0.25, index + 0.35], [0, 1], { clamp: true, ease: easing.easeOut })}
            fill="#0f1117" stroke={accent} strokeWidth={6} />
        ))}
      </svg>
      <div data-zxn-element-id="readout" style={{ position: "absolute", right: 220, top: 140, fontSize: 96, fontWeight: 900, color: accent, fontVariantNumeric: "tabular-nums", opacity: fade }}>
        {Math.round(mapRange(reveal, [0, 1], [readings[0] ?? 0, readings.at(-1) ?? 0]))}
      </div>
    </FullFrame>
  );
}

export const films = [defineFilm({
  id: "drawn-line",
  title: "Drawn line",
  width: 1920,
  height: 1080,
  frameRate: 30,
  frames: 130,
  component: DrawnLine,
  defaultProps: { title: "Weekly renders", accent: "#3ff0d6" },
  controls: {
    title: { type: "text", label: "Title" },
    accent: { type: "color", label: "Accent" },
  },
})];
```

@video(drawn-line)

Tolv cirkler og én sti er en lille DOM, så SVG er det rette medie her. Over et par hundrede markeringer hører samme diagram hjemme på et canvas.

## Næste skridt

- [Tænk i billeder](/da/motion/thinking-in-frames): hvorfor billeder uden for rækkefølge er den begrænsning, alt følger af.
- [Tegn på canvas](/da/motion/canvas): tegn mange ting i én gennemløbning.
- [Kontroller og egenskaber](/da/motion/controls-and-props): gør en film justerbar uden at åbne den.
- [Opskrifter](/da/motion/recipes): de verificerede eksempler, der citeres på denne side, i fuld længde.

