Setup
Import everything from the gitframes npm package. If the project doesn't have gitframes set up yet, follow the gitframes skill first (install, project scaffold, first render).
Overview
gitframes is a code-first, deterministic video composition SDK executing natively on WebGPU (Dawn) in Node.js. It replaces browser-based rendering (Puppeteer/Chromium) with high-performance GPU pipelines, Yoga flexbox layout, Slug vector typography, 3D camera transforms, and reactive audio/signal drivers.
Every composition is defined as a tree of layout nodes compiled into a deterministic execution program: (composition, frame) → pixels.
Architecture & Core Concepts
1. Composition Lifecycle
A Composition instance defines canvas dimensions, frame rate, background, registered fonts, global audio tracks, and the layer hierarchy:
import { Composition, Layer, FontManager } from "gitframes";
// Always register fonts before assembling layers await FontManager.register({ family: "Inter", source: "assets/fonts/Inter.ttf" });
const comp = new Composition({ width: 1920, height: 1080, fps: 30, durationMs: 10000, // or computeDuration() backgroundColor: "#0E0D0C", fonts: ["assets/fonts/Inter.ttf"], });
comp.add(Layer.box({ /* ... */ })); comp.add(Layer.audio("assets/score.mp3", { volume: 1 }));
### 2. Scene Architecture (Production Pattern)
For commercials, films, and multi-shot videos, break the project into discrete, modular scene functions using scene-local frames:
- **Tokens first (`theme.ts`):** Centralize color palette, typography, easing curves, and a tempo-locked beat grid (`BEAT = Math.round(fps * 60 / BPM)`). Never use arbitrary margins or ad-hoc hex codes.
- **Scene modules (`scenes/hero.ts`):** Each scene returns a full-frame root box with `startFrame` and `durationFrames`. Children position themselves relative to scene start (`frame: 0` = scene onset).
- **Master assembly (`film.ts`):** Imports scenes, mounts them to `comp.add(scene())`, attaches the master soundtrack and composition-level grading/grain.
```typescript
// theme.ts
export const W = 1920;
export const H = 1080;
export const FPS = 30;
export const BEAT = 18; // 100 BPM at 30 fps
export const beats = (n: number) => Math.round(n * BEAT);
export const INK = "#0E0D0C";
export const PAPER = "#EEE9E0";
export const EMBER = "#FF5A1F";
export function scene(options: { id: string; from: number; to: number; children: unknown[] }) {
return Layer.box({
id: options.id,
position: "absolute",
x: 0,
y: 0,
width: W,
height: H,
startFrame: options.from,
durationFrames: options.to - options.from,
children: options.children as never,
});
}
Layer Primitives (Layer.*)
All Layer.* builders return AnimatableNode<T> instances with chainable .animate(), .apply(), and effect methods.
| Builder | Node kind |
Description & Key Options |
|---|---|---|
Layer.text(content, options) |
"text" |
Slug GPU vector text. Options: fontSize, fontFamily, fill, letterSpacing, lineHeight, align, animators, spans. |
Layer.box(options) |
"box" |
Rectangular container / card. Options: background, borderRadius, borderColor, borderWidth, overflow: "hidden", padding, children. |
Layer.flex(options) |
"flex" |
Auto-layout container (Yoga). Options: dir: "row" | "column", gap, justify, align, padding, wrap, children. |
Layer.video(src, options) |
"media" |
Video layer decoded via mediabunny. Options: fit: "cover" | "contain", trimStartSec, muted, volume. |
Layer.image(src, options) |
"media" |
Static raster image (.png, .jpg, .webp). Options: fit: "cover" | "contain". |
Layer.svg(src, options) |
"media" |
Vector SVG asset. Options: fit: "contain". |
Layer.lottie(src, options) |
"media" |
Lottie vector animation file. |
Layer.audio(src, options) |
"media" |
Audio track layer. Options: volume, startFrame, durationFrames. |
Layer.shape(type, options) |
"shape" |
Parametric vector graphics ("rect", "circle", "ellipse", "polygon", "star", "arrow", "path"). Options: d (SVG path data with arcs), strokeWidth, strokeColor, trimStart, trimEnd. |
Layer.caption(src, options) |
"media" |
SRT-driven synchronized subtitle engine. Bottom-anchored auto-wrapping cues. |
Layer.chart(options, props) |
"box" |
Charts built from native nodes (d3 scales and geometry). type: "line" | "area" | "bar" | "scatter" | "candlestick" | "pie" | "donut". Options: width, height, series: [{ name, data, color, type, curve, pointRadius }] or data, categories, stacked, xAxis/yAxis (min, max, ticks, grid, format: a d3-format string like "$,.0f"), legend, valueLabels, fontFamily, fontSize, colors, animate: { start, duration, stagger, ease } | false. props positions the chart box. Children get ids <id>-bar-<series>-<i>, <id>-line-<series>, <id>-slice-<i>. Keep scatter charts to a few hundred points: each point is a node. |
Layer.camera(options) |
"camera" |
3D scene camera with FOV, lens rack focus, and orbital coordinates. |
Layer.light(options) |
"light" |
3D lighting primitive (ambient, directional, point, spot). |
Layer.model(options) |
"model3d" |
3D mesh loader (.gltf, .glb, .obj, .fbx, .stl, .ply). Supports skinning and audio deformation compute shaders. |
Animation System (LayerAnimation)
Create animations using LayerAnimation.create() or fluent factory methods. Animations are composed of keyframed tracks or procedural drivers.
1. Keyframe Transitions (fromTo)
import { LayerAnimation } from "gitframes";
// Fluent chain
const anim = LayerAnimation.create()
.fadeIn(0, 20, "power2.out")
.fromTo("y", 60, 0, { start: 0, end: 24, ease: "expo.out" })
.fromTo("scale", 0.95, 1.0, { start: 0, end: 30, ease: "power3.out" });
// Easing bases: none, power1, power2, power3, sine, circ, expo, back, elastic, bounce, spring, cubic, hold
// Directions: in, out, inOut (e.g. "expo.out", "back.out(1.7)", "power2.inOut")
For more than two keyframes on one prop, list them with keys: each tuple is [frame, value, ease?], where the ease shapes the motion into that keyframe. Frames are rounded, so beat math (at + BEAT / 2) can go straight in:
const pop = LayerAnimation.create()
.keys("scale", [[0, 0], [10, 1.12, "back.out(1.6)"], [16, 1, "power2.out"]])
.keys("opacity", [[0, 0], [6, 1]]);
// Static form: LayerAnimation.keys("rotation", [[0, -8], [20, 0, "expo.out"]])
Keyframes past the layer's durationFrames never play; the render warns (animation_truncated) and renderVideo({ qa: true }) reports it.
2. Kinetic Typography (TextAnimator)
Text animators operate at the GPU glyph instancing level, cascading offsets across characters, words, or lines:
import { Layer, LayerAnimation, TextAnimator } from "gitframes";
// Letter-by-letter wave rise with perspective tilt and blur
const headlineNode = Layer.text("PRECISION MOTION", {
fontSize: 96,
fontFamily: "Inter",
fill: "#FFFFFF",
animators: [
TextAnimator.waveRise({
y: 40,
rotationX: 45,
opacity: 0,
blur: 10,
easing: "expo.in"
})
]
}).animate(
// Sweep the selection offset across the text string
LayerAnimation.create().kineticSweep(-1, 1, 0, 24, "power2.inOut")
);
Presets available: TextAnimator.waveRise(), TextAnimator.wordPop(), TextAnimator.blurIn(), TextAnimator.flip3D(), TextAnimator.colorSweep().
3. 3D Spatial Transforms & Camera Motion
Layers can be transformed in 3D perspective space:
const card = Layer.box({
width: 600,
height: 400,
background: "#181715",
borderRadius: 24,
transformStyle: "preserve-3d",
perspective: 1200,
}).animate(
LayerAnimation.create()
.fromTo("rotateX", 25, 0, { start: 0, end: 30, ease: "expo.out" })
.fromTo("rotateY", -35, 0, { start: 0, end: 30, ease: "expo.out" })
.fromTo("translateZ", -200, 0, { start: 0, end: 30, ease: "expo.out" })
);
For full 3D camera staging, use CameraAnimation:
import { CameraAnimation } from "gitframes";
const camAnim = CameraAnimation.camera()
.orbit({
azimuth: { from: -45, to: 15 },
elevation: { from: 10, to: 25 },
radius: { from: 2000, to: 1400 },
start: 0,
end: 90,
ease: "sine.inOut"
})
.rackFocus({
fromDistance: 2000,
toDistance: 1400,
fromFStop: 1.8,
toFStop: 4.0,
start: 20,
end: 60
});
4. Procedural & Audio-Reactive Drivers
Properties can be bound to Perlin wiggles, harmonic springs, or audio-extracted signals:
// Organic floating drift
const floatAnim = LayerAnimation.create()
.wiggle("x", { frequency: 1.5, amplitude: 25 })
.wiggle("y", { frequency: 1.2, amplitude: 20 });
// Physics spring overshoot
const springAnim = LayerAnimation.create()
.spring("scale", { damping: 14, stiffness: 190, mass: 1 });
// Audio reactivity: pass the signal object straight in…
const kick = Signal.fromArray(kickEnvelope, fps); // or Signal.builder({ type: "custom", fn })
const beatAnim = LayerAnimation.create()
.signal("scale", kick, { multiplier: 0.12, offset: 1.0, smoothing: 2 });
// …or register it once by name and bind the name (handy when many layers share it)
comp.addSignal("drum_stem_signal", kick);
const beatAnim2 = LayerAnimation.create()
.signal("opacity", "drum_stem_signal", { multiplier: 0.5, offset: 0.5 });
.signal() / .colorSignal() drive the prop with value × multiplier + offset every frame. A signal
object is carried on its track and registered with the program automatically (under a generated
inline_signal_N id, so the spec stays plain JSON); a string is looked up in comp.addSignal names.
Anything else (a number, undefined) throws. Signals are sampled at the composition frame
(ctx.frame), not the layer's local frame.
Critical Authoring Rules & Gotchas
- Local Keyframe Coordinates: Keyframes are strictly relative to the node's own timeline window (
0= node appearance). Never offset keyframe frames by the parent'sstartFrame. - Coordinate Overrides: Animating
yoverrides the node's layouty. If a node is placed aty: 400, animatefromTo("y", 440, 400), NOTfromTo("y", 40, 0). - No String Styles for Borders: Use
borderColorandborderWidth. CSSborder: "1px solid red"is silently ignored. - Font Registration is Mandatory: In headless WebGPU, system fonts do not exist. Every font must be registered via
FontManager.register()and listed incomp.fonts. - Integer Frames Only: Keyframe frames, start frames, and durations must be integers. Decimal frames cause schema validation errors.
- Relative vs. Absolute: Providing
xorywithoutposition: "relative"turns the node into an absolute-positioned layer removed from flex flow. - Audio Layer Duration: Every
Layer.audio()must be assigneddurationFramesto prevent audio truncation in headless rendering. - Premultiplied Alpha: Alpha shaders operate with premultiplied alpha. Never use
"src-alpha"factor in custom blends.