Skip to content

Core concepts

View .mdllms.txt

A composition compiles to a deterministic program: (composition, frame) → pixels. There are no timers and no wall clock. Rendering frame 45 always produces the same image, in any order, on any run. This is what makes frames testable and renders parallel.

import { Composition } from "framefields";
const comp = new Composition({
width: 1920,
height: 1080,
fps: 30, // default 60
durationMs: 10_000, // or durationFrames; omitted → computed from media
backgroundColor: "#0e0d0c", // default "#000000"
antialias3d: true, // 4x MSAA for 3D meshes; false saves GPU memory
});
Method Does
add(layer | layer[]) adds layers (or arrays, e.g. from Lighting.studio()) to the root
addAudio(Layer.audio(...)) adds a soundtrack
apply(effect) applies an effect to the whole frame
addSignal(name, signal) registers a named signal layers can bind by name
withVision(options) runs vision on the composition, returns reactive signals
lint() document checks, no GPU
toSpec() the plain-JSON program (validate with CompositorProgramSchema)
renderFrame / renderFrameGrid / renderVideo output, see Rendering
computeDuration() duration in ms, from options or media

Every Layer.* builder returns a node you can chain: .animate(), .withEffect(), .withMatte(), .withVision(), .pinToObject(). Layers nest through children.

Layer.box({
width: 720, height: 380, padding: 32,
background: "#141721", borderRadius: 24,
borderColor: "#262b3d", borderWidth: 1.5,
children: [
Layer.flex({
dir: "column", gap: 16,
children: [
Layer.text("ENGINE", { fontSize: 16, fontWeight: 700, fill: "#6366f1", letterSpacing: 2 }),
Layer.text("WebGPU motion", { fontSize: 48, fontWeight: 700, fill: "#f8fafc" }),
],
}),
],
});
  • startFrame and durationFrames place a layer on its parent’s timeline.
  • Keyframes are local: frame 0 is the layer’s first visible frame. Never add the parent’s startFrame to keyframe times.
  • Frames are integers. Keyframe frames, start frames and durations must be whole numbers; .keys() rounds for you, other APIs reject decimals.
  • Keyframes past a layer’s durationFrames never play (lint/QA: animation_truncated).
  • Signals are sampled at the composition frame, not the layer’s local frame.

A beat grid keeps timing musical and readable:

export const FPS = 30;
export const BPM = 100;
export const BEAT = Math.round((FPS * 60) / BPM); // 18 frames
export const beats = (n: number) => Math.round(n * BEAT);
  • Containers lay children out with flexbox (Yoga): Layer.flex({ dir, gap, justify, align, wrap, padding }).
  • Setting x or y without position: "relative" makes a layer absolute — removed from flex flow, positioned in its parent.
  • Animating x/y overrides layout. A layer placed at y: 400 animates with fromTo("y", 440, 400), not fromTo("y", 40, 0).
  • Borders are borderColor + borderWidth. A CSS shorthand like border: "1px solid red" is ignored.
  • 2D: pixels, origin top-left, y down.
  • 3D: the camera is calibrated so z = 0 maps 1:1 onto canvas pixels. y is still down, so “up” is −y. See 3D.

Every color prop takes hex, rgb(), hsl(), oklch(), oklab() or a CSS name. Color keyframes blend in OKLab, so red → green passes through yellow. Gradients are CSS strings (linear-, radial-, conic-gradient()) usable as any background or shape fill.

src/theme.ts tokens: size, fps, colors, fonts, beat grid
src/scenes/*.ts one function per scene, each returns a full-frame box
src/film.ts buildFilm(): fonts, Composition, Layer.sequence(scenes), grade
src/render.ts frames | grid | preview | video
assets/ fonts, footage, audio, images, LUTs, models — in Git LFS
output/ renders, gitignored

Media in assets/ is stored with Git LFS: push it with a normal git push, and run git lfs pull in any fresh checkout before rendering. See Assets and Git LFS.