Opskrifter
Otte verificerede eksempelfilm, hvad hver lærer dig, linjen, der er værd at kopiere, og hvad du først bør ændre.
Hver film her ligger i examples/recipes/ og har én opgave: at vise én teknik i den mindste film, der stadig ligner noget, du ville udgive. Læs den, der ligger nærmest dit problem, kopiér den, og ændr props.
| Opskrift | Hvad den lærer dig | Teknik | Pakker |
|---|---|---|---|
| Matrix-regn | Et felt med tusindvis af ting koster ét element | Canvas | — |
| Ur med tidsforløb | Tid, der går, uden at afspille hele forløbet | SVG | — |
| Tegnefilmsfjernsyn | Sammentrykning, strækning og efterbevægelse | SVG + Canvas | — |
| Søjlediagramræs | Rene biblioteker interpoleret ud fra billednummeret | DOM | d3-scale, d3-interpolate |
| Støjfelt | Fast startværdi til et generativt bibliotek | Canvas | simplex-noise |
| GSAP-titel | Sæt tidslinjens position, afspil den aldrig | DOM | gsap |
| Navnebjælke | Indgang, pause og udgang, alt styret af kontroller | DOM | — |
| Slagpuls | Klipning på takter og slag | DOM | culori |
Disse ved vi virker
examples/recipes/recipes.test.tsx monterer hver opskrift i en rigtig DOM ved dens første, midterste og sidste billede samt ét billede efterspurgt uden for rækkefølge. En opskrift, der kaster en fejl, logger en React-fejl, næsten intet renderer eller intet tegner på sit canvas, får testsættet til at fejle. Det kontrollerer også, at samme billede efterspurgt to gange giver samme markup, og at hver standardprop har en kontrol. Derefter renderes hver opskrift til videoen på denne side. Derfor er uddragene nedenfor citeret fra filerne frem for skrevet af: Et eksempel kan ikke ændre sig uafhængigt af siden, der citerer det.
Render selv en:
zxn-motion render examples/recipes/retro-tv.tsx retro-tv out/retro-tv.mp4
Hver fil eksporterer både en navngivet film og et films-array, så den er et gyldigt CLI-indgangsmodul i sig selv.
Matrix-regn
Digital regn: strømme af tegn, der bevæger sig trinvist ned over skærmen, hver med sin egen hastighed, længde og start, et næsten hvidt hoved og et spor, der toner ud bagved. Den lærer dig den ene beslutning, der afgør, om en film renderes på et minut eller en time — et felt af mange små ting er ét element, ikke mange. Den oplagte løsning, en <div> pr. tegn med en lysende text-shadow, giver et par tusind slørede elementer, som skal have beregnet layout og tegnes på hvert billede.
const columns = Math.ceil(width / cell);
const rows = Math.ceil(height / cell);
for (let column = 0; column < columns; column += 1) {
// Everything about a stream comes from its column number, so any frame can be drawn on its own.
const pace = speed * (0.45 + 0.55 * seededRandom(`pace-${column}`));
const length = 8 + Math.floor(seededRandom(`length-${column}`) * 22);
// A short gap before a stream comes round again keeps the field full without ever looking like a wall.
const cycle = rows + length + Math.floor(seededRandom(`gap-${column}`) * rows * 0.4);
const head = Math.floor(seconds * pace + seededRandom(`start-${column}`) * cycle) % cycle;
Intet gemmes mellem billeder. En strøms hastighed, længde og fase aflæses alle fra seededRandom ud fra kolonnenummeret, så billede 200 kan tegnes uden først at tegne billeder 0 til 199 — hvilket er det, tidslinjeskyderen gør, og det, en renderer må gøre. Math.floor(seconds * pace) får regnen til at springe en hel celle ad gangen i stedet for at glide; glidning opleves som faldende tekst, trinvise spring som regnen.
Ændr først: speed og cell i kontrollerne — mindre celler giver større tæthed, og cell er også tegnstørrelsen og glødens radius. Derefter konstanten glyphs, som er hele tegnsættet. Gå videre: Tegn på canvas og Ydeevne.
Ur med tidsforløb
Et analogt ur, der bevæger sig fra ét tidspunkt til et andet, med en digital visning under urskiven og teksten »tre timer senere«, der kommer til sidst. Det lærer dig at vise et langt tidsrum uden at afspille hele forløbet: Viserne tikker ikke gennem tre timer; de bevæger sig blødt gennem dem. Det er også modstykket til regnen — et dusin skarpe former er præcis det, SVG er godt til.
const appear = springValue({ frame, frameRate: film.frameRate, stiffness: 120, damping: 14 });
// Hold on the start time, sweep, then hold on the end so both can be read.
const progress = mapRange(frame, [24, film.frames - 36], [0, 1], { clamp: true, ease: easing.easeInOut });
const hours = from + (to - from) * progress;
const captionIn = mapRange(frame, [film.frames - 40, film.frames - 22], [0, 1], { clamp: true, ease: easing.easeOut });
const hourAngle = (hours % 12) * 30;
const minuteAngle = ((hours * 60) % 60) * 6;
Ét tal, hours, styrer timeviseren, minutviseren og cifrene, så de ikke kan være uenige — der er ingen anden beregning at holde i takt. Støttepunkterne er skrevet relativt til film.frames, så en længere film forlænger bevægelsen og lader pausen til sidst være uændret. easeInOut er pointen i grebet: langsomt, hurtigt, langsomt er den måde, et ur, der sætter farten op, altid er blevet tegnet på.
Ændr først: from og to (timer med decimaldel, så 9,5 er 09:30) og caption. Derefter støttepunkterne [24, film.frames - 36], som er hele filmens rytme. Gå videre: Tænk i billeder og Kontroller og egenskaber.
Tegnefilmsfjernsyn
Et tegnefilmsfjernsyn fra 1960'erne, der falder ind, skyder forbi, trykkes sammen ved landing og vipper med antennerne et øjeblik senere, med støj på skærmen og en afspilningsknap, der trykkes på. Det lærer dig karakter: sammentrykning, strækning og efterbevægelse. Det viser også begge teknikker i én film — SVG til apparatet og et lille <Canvas2D> til støjen, fordi »et par tusind tilfældige prikker, der skifter på hvert billede« er det, DOM er allerdårligst til.
// The drop: a loose spring, so it overshoots and settles like something with weight.
const drop = springValue({ frame, frameRate: fps, stiffness: 140, damping: 11 });
const landing = Math.max(0, 1 - Math.abs(drop - 1) * 6) * mapRange(frame, [6, 22], [1, 0], { clamp: true });
const squash = 1 - 0.12 * landing;
// The antennae follow the body a few frames late, which is what makes them look attached by springs.
const lag = springValue({ frame: frame - 5, frameRate: fps, stiffness: 90, damping: 7 });
const wobble = (1 - lag) * 38;
landing er en spids: Den er kun nær 1, mens drop er tæt på sit mål, og mapRange toner hele effekten ud efter billede 22, så kabinettet trykkes sammen ved første landing og ikke ved hver senere svingning fra samme fjeder. Efterbevægelsen er én linje — antennerne aflæser samme fjeder ved frame - 5. Begge virker, når du trækker afspilningsmarkøren, fordi springValue bruger en lukket formel; en trinvis simulering ville ikke kunne sige noget om billede 100, før den havde kørt billeder 0 til 99.
Ændr først: de fire farvekontroller, derefter pressAt, det billede, hvor afspilningsknappen trykkes på, som trækker støjen, gløden og knappens egen fjeder med sig. Gå videre: Tænk i billeder og Tegn på canvas.
Søjlediagramræs
Søjler, der vokser og overhaler hinanden mellem årlige øjebliksbilleder. Det lærer dig at bruge et bibliotek, der består af rene funktioner: d3-scale og d3-interpolate tager et tal og giver et tal tilbage, præcis som en film ønsker. Den må ikke bruge en d3-overgang — de kører på det rigtige ur.
// Bars trade places quickly, in the middle of the step: two bars sharing a row is unreadable, so they
// spend as little time crossing as looks natural, and the one moving up rides on top while they do.
const swap = easing.easeInOut(mapRange(within, [0.3, 0.7], [0, 1], { clamp: true }));
const before = rankIn(from.values, name);
const after = rankIn(to.values, name);
const rank = interpolateNumber(before, after)(swap);
const top = chart.top + (y(0) ?? 0) + rank * y.step();
Placeringen interpoleres som et tal frem for at blive skiftet brat, så en søjle glider gennem rækken ovenover i stedet for at hoppe ind i den. Byttet presses ind i de midterste 40 % af trinnet, fordi to søjler på samme række er den utydelige del, og zIndex: after < before ? 2 : 1 placerer den vindende søjle øverst, mens de krydser. Hver søjle er bevidst et rigtigt DOM-element med et data-zxn-element-id: Et diagram indeholder et dusin ting, man kan navngive, og en vært kan så vælge en af dem.
Ændr først: arrayet snapshots og framesPerStep — filmens længde udledes af begge, så filmen vokser med dataene. Derefter colors og kontrollerne title/unit. Gå videre: Brug af pakker.
Støjfelt
Et drivende felt af korte linjer, hver vinklet af simplex-støj, med tid som støjens tredje akse. Det lærer dig det ene, der går galt med et generativt bibliotek: Giv det en fast startværdi, ellers får du en ny film ved hver rendering.
let draws = 0;
const noise = createNoise3D(() => seededRandom(`noise-field-${draws++}`));
createNoise3D() uden argument bygger sin permutationstabel fra Math.random, så feltet er forskelligt i forhåndsvisningen, når du trækker afspilningsmarkøren, og i eksporten. En deterministisk kilde løser det, og oprettelse én gang på modulniveau betyder, at tabellen ikke genopbygges pr. billede. Sekunder som tredje akse frem for som en forskydning får feltet til at drive: Nærliggende billeder er nærliggende punkter i samme glatte rumfang, så intet hopper.
Ændr først: kontrollen drift, derefter spacing i tegnefunktionen (tættere koster mere, men det er ét element uanset hvad) og gradientfarverne from/to. Gå videre: Brug af pakker og Tegn på canvas.
GSAP-titel
En titel koreograferet med GSAP: En streg tegnes ind, ord stiger op forskudt, en undertitel glider ind, og det hele forlader billedet igen. Det lærer dig reglen for alle tidsbaserede biblioteker — opret det pauset, og sæt dets position; afspil det aldrig.
// After the build effect on the first frame, and on every frame after it.
useLayoutEffect(() => { timeline.current?.seek(seconds, false); });
useVirtualTime() giver sekunderne for det billede, der renderes, og GSAP genberegner hele tidslinjens tilstand for det øjeblik fra bunden, så billeder kan efterspørges i enhver rækkefølge. Positionen sættes i en layout-effekt uden afhængighedsarray, så den kører efter oprettelseseffekten på første billede og efter hver opdatering derefter — før værten tager billedet. Samme mønster passer til alt, hvis position kan sættes: lottie.goToAndStop(frame, true), Web Animations' currentTime og en videos currentTime. Et bibliotek, der kun kan afspille, kan ikke bruges i en film.
Ændr først: line1, line2 og accent, derefter varighederne og overlap med "-=0.25" i tidslinjen samt 3.4, som begge udgangsanimationer deler — det tal er der, hvor filmen vender om. Gå videre: Brug af pakker.
Navnebjælke
Navnebjælken, der vises under en person, som taler: en farvet streg, et navn og en stilling, ind og ud over en gennemsigtig baggrund, så den kan lægges over videomateriale. Den lærer dig at skrive indgang, pause og udgang som ét læseligt udtryk, og hvorfor hver prop fortjener en kontrol.
const last = film.frames - 1;
const slide = springValue({ frame, frameRate: film.frameRate, stiffness: 150, damping: 20, clamp: true });
const leave = mapRange(frame, [last - 16, last], [0, 1], { clamp: true, ease: easing.easeIn });
const text = mapRange(frame, [8, 22, last - 20, last - 8], [0, 1, 1, 0], { clamp: true, ease: easing.easeOut });
mapRange tager tilsvarende lister af støttepunkter, så fire af dem beskriver indtoning, pause og udtoning på én linje. Alt udtrykkes i forhold til last, så ændring af frames flytter udgangen og lader indgangen blive, hvor den er. Teksten starter otte billeder efter bjælken, hvilket er hele tricket ved en god navnebjælke: Beholderen ankommer, og så fylder ordene den.
Ændr først: name, role, accent og side — alle fire er kontroller, så en redaktør aldrig åbner filen. Derefter damping: 20 på indglidningen, som styrer, hvor meget bjælken skyder forbi. Gå videre: Kontroller og egenskaber.
Slagpuls
Fire ord skifter ét pr. takt, en ring pulserer ud på hvert slag, fire prikker viser tællingen, og baggrunden toner fra én farve til en anden. Det lærer dig at placere ting i takter og slag frem for billednumre.
// How far through the current beat we are: 0 on the beat, 1 just before the next.
const beat = tempo.beatAt(frame);
const sinceBeat = frame - tempo.beat(Math.floor(beat));
const kick = mapRange(sinceBeat, [0, 7], [1, 0], { clamp: true, ease: easing.easeOut });
tempo.beat(n) konverterer et slagindeks til et billede, og tempo.beatAt(frame) går den anden vej, så sinceBeat er filmens afstand forbi sidste nedslag uanset bpm. Hver konvertering går gennem samme funktion, og derfor lander ordskiftet og pulsen på samme billede i stedet for med ét billedes mellemrum. Ordene placeres med <Cue startBar={index} lengthBars={1}> — ingen billednumre nogen steder — så ændring af bpm i filmens tempo ændrer tidsforløbet af hele filmen.
Ændr først: bpm i tempo og frames ved siden af. Derefter arrayet words og farvekontrollerne from/to, som blandes i OKLCH af culori, så midtpunktet forbliver klart i stedet for at gå gennem gråt. Gå videre: Tempo og musik.
Idéer til det næste, du kan bygge
Hver af disse ligger én teknik fra noget ovenfor.
- En vinylplade med tonearm. Etiketten er en roterende SVG-gruppe; armen er én fjeder, der svinger ind på indløbsrillen og derefter langsomt kryber henover med et
mapRange. - En raketnedtælling, der ryster ved nul. Cifre på
<Cue>-komponenter med ét sekunds mellemrum, derefter en rystelse med fast startværdi —seededRandommed billednummeret som nøgle, der styrer en forskydning — som aftager over cirka tolv billeder. - En skrivemaskine med vognretur.
text.slice(0, charactersAt(frame))på ét element, en markør, der blinker ud fraframe % 20, og hele blokken, der glider én linje op, når tælleren passerer et linjeskift. - En indlæsningsbjælke, der lyver. Ét
mapRangemed seks støttepunkter: hurtigt til 70 %, stilstand, langsomt frem og så et spring til 100 — støttepunkterne er vittigheden. - Et vejrkort med faldende sne. Kortet er DOM, sneen er ét
<Canvas2D>tegnet ud fra konstanter pr. snefnug med fast startværdi, ligesom regnen. - En abonnementsknap, som en markør trykker på.
<Stage>med et layout, en<Pointer>-sti og knappen, der tegner sin egen trykkede tilstand ud frausePointer(path).pressedOn("subscribe"). - Et kapløb mellem to fremdriftsbjælker. Interpolationen fra søjlediagramræset med to rækker og en målfotoafgørelse: Lad de sidste 10 % bremse langt mere op end de første 90 %.
- Et polaroidfoto, der fremkaldes. Ét billede under et hvidt lag, hvis
opacityogfilter: saturate()udledes af billednummeret, plus en lille fjederbevægelse, når det lander på bordet. - En terminal, der skriver en kommando. DOM med monospace-skrift, skrivemaskinens tekstudsnit til prompten og derefter outputlinjer afsløret af stablede
<Cue>-komponenter med to billeders mellemrum. - En urskive, der bliver til et cirkeldiagram. Urskiven fra uret med tidsforløb, hvor sektoren vokser til en faktisk dataværdi, og viserne toner ud under den.
Start med den nærmeste opskrift, kør zxn-motion render på den, og ændr ét tal ad gangen. Arbejder du med en AI-agent, giver Skriv film med en AI-agent den alt dette via MCP.