Start her

Kom godt i gang

Byg biblioteket fra kildekoden, skriv din første film, se den i browserafspilleren, og render den til MP4 eller WebM fra kode eller kommandolinjen.

En film er en React-komponent, der tegner ét billede: Den får et heltalsnummer på et billede og nogle JSON-props og returnerer billedet for præcis det billede. Fordi en film aldrig aflæser et ur eller gemmer tilstand mellem billeder, ser billede 47 ens ud, uanset om du har trukket afspilningsmarkøren hen til det, afspillet frem til det eller eksporteret det.

Det er hele idéen. Alt andet her — afspilleren, rendereren og CLI'et — er mekanik til at bede en film om billeder og gøre noget med svarene.

Installation

Biblioteket er endnu ikke på npm, så du bygger det fra kildekoden. Du skal bruge Node 22 eller nyere.

git clone https://github.com/zinxan/motion-graphics.git
cd motion-graphics
npm install
npm run check      # typecheck, tests, build

npm install forbinder pakkerne i arbejdsområdet, så @zxn/motion-core, @zxn/motion-player, @zxn/motion-renderer og @zxn/motion-cli kan findes ved navn overalt i kodearkivet. Arbejd i examples/, eller peg din egen apps bundler på de byggede pakker.

Din første film

En film er defineFilm({...}) omkring en komponent. Definitionen indeholder de oplysninger, en vært skal bruge, før den renderer noget: størrelsen, billedfrekvensen, antallet af billeder og de props, komponenten forventer. Et indgangsmodul eksporterer dem som et array med navnet films.

import { FullFrame, defineFilm, easing, mapRange, springValue, useTimeline } from "@zxn/motion-core";

type Props = {
  readonly headline: string;
  readonly subtitle: string;
  readonly accent: string;
};

function Opener({ headline, subtitle, accent }: Props) {
  const { frame, film } = useTimeline();
  const last = film.frames - 1;
  const rise = springValue({ frame, frameRate: film.frameRate, stiffness: 150, damping: 18, clamp: true });
  const sub = mapRange(frame, [10, 26], [0, 1], { clamp: true, ease: easing.easeOut });
  const out = mapRange(frame, [last - 18, last], [1, 0], { clamp: true, ease: easing.easeIn });

  return (
    <FullFrame style={{ background: "#0b0c0e", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 24, opacity: out }}>
      <h1 style={{ margin: 0, fontFamily: "Inter, system-ui, sans-serif", fontSize: 150, fontWeight: 900, letterSpacing: "-0.04em", color: "#f3efe7", transform: `translateY(${(1 - rise) * 90}px)` }}>
        {headline}
      </h1>
      <p style={{ margin: 0, fontFamily: "Inter, system-ui, sans-serif", fontSize: 52, fontWeight: 500, color: accent, opacity: sub, transform: `translateY(${(1 - sub) * 20}px)` }}>
        {subtitle}
      </p>
    </FullFrame>
  );
}

export const films = [defineFilm({
  id: "opener",
  title: "Opener",
  width: 1920,
  height: 1080,
  frameRate: 30,
  frames: 120,
  component: Opener,
  defaultProps: { headline: "Frame by frame", subtitle: "A film is a function of time", accent: "#7a6cff" },
  controls: {
    headline: { type: "text", label: "Headline" },
    subtitle: { type: "text", label: "Subtitle" },
    accent: { type: "color", label: "Accent" },
  },
})];

Fire ting i filen er værd at fremhæve.

useTimeline() returnerer { frame, absoluteFrame, film }. frame er det billede, der tegnes, og film indeholder width, height, frameRate og frames. Intet andet i komponenten ved, hvad klokken er.

<FullFrame> fylder filmen og er en flex-container, og derfor centrerer alignItems og justifyContent teksten.

springValue og mapRange omsætter billednummeret til de tal, stylingen har brug for. springValue bruger en lukket formel: Den beregner fjederens position ved et billede i stedet for at føre en simulering frem trin for trin. Derfor giver det samme billede at trække afspilningsmarkøren ind midt i bevægelsen som at rendere sekventielt. mapRange tager tilsvarende lister med to eller flere støttepunkter, så ét kald kan beskrive en indgang, en pause og en udgang.

controls beskriver hver prop for en vært, der vil vise et egenskabspanel — text, number, color, boolean eller choice. Giv hver standardprop en kontrol, så kan en redaktør ændre filmen uden at åbne koden. Props skal være JSON, fordi de gemmes i projektfiler, sendes på kommandolinjen og overføres mellem processer.

Ved 30 fps er frames: 120 en film på fire sekunder.

Se en forhåndsvisning

@zxn/motion-player giver dig <FilmPlayer>: et visningsområde, der skalerer filmen, så den passer, en afspil/pause-knap, genstart og en tidslinjeskyder. Den tager en FilmDescriptor, som er det, describeFilm() returnerer.

import { describeFilm } from "@zxn/motion-core";
import { FilmPlayer } from "@zxn/motion-player";
import "@zxn/motion-player/player.css";
import { films } from "./films";

export const Preview = () => <FilmPlayer film={describeFilm(films[0]!)} loop />;

controls={false} skjuler afspilningskontrollerne, inputProps tilsidesætter filmens standardværdier, og onFrameChange fortæller, hvor afspilningsmarkøren er. Afspilleren leveres med sit eget lille stilark; der er ingen Tailwind at konfigurere.

Træk i tidslinjeskyderen. En film, der ser rigtig ud under afspilning og forkert ud, når du trækker i skyderen, aflæser noget andet end billednummeret. Det er netop den fejl, hele designet skal gøre synlig tidligt.

Render den i browseren

@zxn/motion-renderer monterer det samme træ som afspilleren, færdiggør hvert præcist billede, rasteriserer DOM'en og koder det via WebCodecs. Spørg først, om enheden kan kode det, du ønsker:

import { canRenderFilm, renderFilm } from "@zxn/motion-renderer";
import { describeFilm } from "@zxn/motion-core";
import { films } from "./films";

const film = describeFilm(films[0]!);
if (await canRenderFilm(film, "mp4-h264")) {
  const bytes = await renderFilm({
    film,
    format: "mp4-h264",                      // also "mov-h264", "webm-vp9"
    onProgress: ({ ratio }) => console.log(ratio),
  });
}

renderFilm returnerer asynkront et Uint8Array med den færdige fil. Den tager også inputProps og et AbortSignal som signal. Hvis enheden ikke kan kode formatet, hvis et billede kaster en fejl, eller hvis encoderen intet returnerer, afvises kaldet med en fejl, der angiver problemet. Den skifter ikke til et andet codec og giver dig ikke et sort billede — et erstatningsformat, ingen har bedt om, er værre end en fejl.

Render den fra kommandolinjen

@zxn/motion-cli bundler filmens indgangsmodul og renderer den i et skjult Electron-vindue i en sandkasse, hvilket passer til CI og scripts.

zxn-motion render ./films.tsx opener ./out/opener.mp4
zxn-motion render ./films.tsx opener ./out/opener.webm --props ./props.json
zxn-motion render ./films.tsx opener ./out/opener.mp4 --force

De tre positionsargumenter er indgangsmodulet, filmens id og outputstien. Filendelsen vælger forudindstillingen: .mp4 og .mov er H.264, og .webm er VP9. Alt andet giver en fejl i stedet for et gæt.

--props <file.json> fletter et JSON-objekt ind i filmens defaultProps. --force kræves for at erstatte en eksisterende fil, og den gamle fil bevares, indtil erstatningen er renderet. --footage <id>=<file> leverer en video til hver <Footage asset="id" /> i filmen; flaget gentages én gang pr. mediefil. --help viser resten.

Tilgængelige codecs afhænger af værtens operativsystem og Electron-build. Som i browseren fejler CLI'et med en forklaring i stedet for at erstatte et codec.

Sådan hænger pakkerne sammen

Pakke Det får du
@zxn/motion-core defineFilm, hooks til billedstyring, grundelementer til tidsstyring, animationsmatematik, Canvas2D og værktøjerne til film om brugergrænseflader: Stage, Camera, Pointer, Footage.
@zxn/motion-player <FilmPlayer>: afspilning, pause, søgning og gentagelse i browseren.
@zxn/motion-renderer renderFilm() og canRenderFilm().
@zxn/motion-cli zxn-motion render.
@zxn/motion-graphics Deklarative diagrammer og fremhævelser, der er deterministiske for hvert billede.
@zxn/motion-text Serialiserbare 2D-tekststile med én renderer til canvas og eksport.

Afhængighederne peger indad: CLI → renderer og afspiller → core. Core har ingen afhængighed af afspiller, renderer, Electron eller filsystem, så en vært, der kun skal definere og montere film, behøver kun core.

Afspilleren og rendereren monterer begge FilmSurface, den samme komponent, ved ét præcist billede. Derfor stemmer forhåndsvisningen og eksporten overens: Der er ingen anden implementering, der kan komme ud af trit.

Næste skridt

  • Tænk i billeder — den mentale model, grundelementerne til tidsstyring og vejen til let aflæselige bevægelser.
  • Kompositions-API — referencen: film, tid, scene og kamera, markør, tempo og videomateriale.
  • Kontroller og egenskaber — design filmens input, så et menneske eller en maskine kan styre den.
  • Tegn på canvas — når DOM er det forkerte værktøj, og <Canvas2D> er det rigtige.
  • Brug af pakker — d3, GSAP, Lottie, simplex-noise og culori inde i en film.
  • Tempo og musik — klipning efter takter og slag.
  • Film om brugergrænseflader — scene, kamera og markør til at vise software.
  • Ydeevne — hvad et billede koster, og hvordan du bruger færre ressourcer.
  • Opskrifter — otte verificerede eksempelfilm, du kan læse og låne fra.
  • AI-agenter — generering af film og kontrollen, der bedømmer dem.