Loading...
Loading...
Start a new gitframes video project, or add gitframes to an existing Node.js project. Use when the user wants to make a video with gitframes and the project has no gitframes setup yet, or asks how to install gitframes, scaffold a composition and render script, or get from an empty folder to a first MP4. Hands off to gitframes-compose, gitframes-effects, and gitframes-render for the details.
npx skill4agent add gatewai-dev/gitframes gitframesgitframesCompositiongitframes| Task | Skill |
|---|---|
| Scenes, layout, typography, animation, beat grids, film structure | |
| Post-processing, color grading, VFX, on-device vision (tracking, cutouts, pose) | |
| Frame inspection, frame grids, pixel tests, video export and QA | |
node --versionpackage.json"type": "module"pnpm-lock.yamlyarn.lockbun.lockpackage-lock.jsonpackage.jsonskia-canvasimport "gitframes"Cannot find module '.../skia.node'package.jsonpackage.json{
"name": "my-film",
"private": true,
"type": "module",
"allowScripts": {
"skia-canvas": true,
"sharp": false,
"webgpu": false,
"onnxruntime-node": false,
"node-av": false,
"esbuild": false
},
"pnpm": {
"onlyBuiltDependencies": ["skia-canvas"],
"ignoredBuiltDependencies": ["sharp", "webgpu", "onnxruntime-node", "node-av", "esbuild"]
},
"trustedDependencies": ["skia-canvas"]
}allowScriptspnpmtrustedDependenciesnpm install gitframes
npm install --save-dev tsx typescript @types/nodeinstall-scriptsnpm rebuild skia-canvaspnpm rebuild skia-canvasnpm install-scripts approve --alltsconfig.json{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext",
"target": "ES2022",
"strict": true,
"skipLibCheck": true,
"noEmit": true,
"types": ["node"]
},
"include": ["src/**/*"]
}src/
theme.ts # canvas size, fps, colors, fonts: the only place for magic numbers
film.ts # buildFilm(): assembles the Composition
render.ts # renders frames for review, a live preview, or the full video
assets/
fonts/ # .ttf / .otf files the film uses
output/ # renders (add to .gitignore)assets/fonts/// src/theme.ts
import { FontManager } from "gitframes";
export const W = 1920;
export const H = 1080;
export const FPS = 30;
export const INK = "#0E0D0C";
export const PAPER = "#EEE9E0";
export const SANS = "Inter";
export async function registerFonts(): Promise<void> {
await FontManager.register({ family: SANS, source: "assets/fonts/Inter.ttf" });
}// src/film.ts
import { Composition, Layer, LayerAnimation } from "gitframes";
import { FPS, H, INK, PAPER, registerFonts, SANS, W } from "./theme.js";
const DURATION = 3 * FPS; // frames
export async function buildFilm(): Promise<Composition> {
// Fonts must be registered before any text layer is built.
await registerFonts();
const film = new Composition({
width: W,
height: H,
fps: FPS,
durationMs: (DURATION / FPS) * 1000,
backgroundColor: INK,
});
film.add(
Layer.text("Hello, gitframes", {
id: "title",
position: "absolute",
x: 0,
y: H / 2 - 60,
width: W,
height: 120,
fontFamily: SANS,
fontSize: 96,
fontWeight: 700,
fill: PAPER,
align: "center",
}).animate(
LayerAnimation.create()
.fadeIn(0, 20, "power2.out")
.fromTo("y", H / 2 - 20, H / 2 - 60, { start: 0, end: 30, ease: "expo.out" }),
),
);
return film;
}// src/render.ts
import fs from "node:fs/promises";
import path from "node:path";
import { HeadlessMediaRenderer, startPreview } from "gitframes";
import { buildFilm } from "./film.js";
const OUT = path.resolve(import.meta.dirname, "../output");
const [mode = "video", ...args] = process.argv.slice(2);
if (mode === "preview") {
// The page runs film.ts itself and renders it with WebGPU in the browser.
const session = await startPreview({ entry: new URL("./film.ts", import.meta.url), export: "buildFilm" });
console.log(`Preview at ${session.url}`);
await session.closed; // until its tab closes or the next preview replaces it
process.exit(0);
}
const film = await buildFilm();
await fs.mkdir(OUT, { recursive: true });
if (mode === "frames") {
const renderer = new HeadlessMediaRenderer();
for (const frame of args.map(Number)) {
const png = await film.renderFrame({ frame, renderer });
await fs.writeFile(path.join(OUT, `f${String(frame).padStart(4, "0")}.png`), png);
}
} else if (mode === "grid") {
const png = await film.renderFrameGrid({
frames: args.length ? args.map(Number) : [0, 10, 20, 30, 45, 60, 75, 89],
columns: 4,
cellWidth: 480,
showLabels: true,
});
await fs.writeFile(path.join(OUT, "grid.png"), png);
} else {
const result = await film.renderVideo({
outputPath: path.join(OUT, "output.mp4"),
quality: "high",
});
await result.cleanup?.();
console.log(`Exported ${result.filePath}`);
}
// Native GPU and encoder threads keep Node alive; always exit explicitly.
process.exit(0);package.json"scripts": {
"typecheck": "tsc --noEmit -p .",
"frames": "tsx src/render.ts frames",
"grid": "tsx src/render.ts grid",
"preview": "tsx src/render.ts preview",
"render": "tsx src/render.ts"
}npm run typechecknpm run frames -- 0 30 60output/npm run gridnpm run previewnpm run rendergitframes-renderrenderVideo({ qa })npm run previewfilm.tsstartPreview(..., { open: true })film.tspath.resolve(path.dirname(fileURLToPath(import.meta.url)), "../assets/fonts")process.cwd()package.jsonrootstartPreviewgitframes-rendergitframes-composegitframes-effectsgitframes-rendertheme.ts