gitframes
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinesegitframes
gitframes
Overview
概览
gitframesCompositiongitframesThis skill takes a project from nothing to a verified first render. For anything past that, load the specialist skill:
| 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 | |
gitframesCompositiongitframes此 skill 指导项目从零搭建,直到完成并验证首次渲染。完成首次渲染后,请加载相应的专业 skill:
| 任务 | Skill |
|---|---|
| 场景、布局、字体排印、动画、节拍网格、影片结构 | |
| 后期处理、调色、VFX、设备端视觉处理(跟踪、抠像、姿态) | |
| 帧检查、帧网格、像素测试、视频导出和 QA | |
1. Check the environment
1. 检查环境
- Node.js 22 or later. Run . Older versions fail at import.
node --version - A WebGPU-capable GPU. macOS uses Metal; Linux and Windows use Vulkan. Headless CI machines without a GPU can't render.
- An ES module project. The project's needs
package.json."type": "module"
- Node.js 22 或更高版本。 运行 。版本过旧会在导入时失败。
node --version - 支持 WebGPU 的 GPU。 macOS 使用 Metal;Linux 和 Windows 使用 Vulkan。没有 GPU 的无头 CI 机器无法渲染。
- ES module 项目。 项目的 需要设置
package.json。"type": "module"
2. Install
2. 安装
Use the package manager the project already uses (look for , , , or ).
pnpm-lock.yamlyarn.lockbun.lockpackage-lock.jsonWrite before installing. npm 11, pnpm 10, and bun block dependency install scripts unless the project approves them, and gitframes needs exactly one: downloads its native binary in its install script. Without it, fails with . The other packages that ship install scripts work without them, so deny them to keep installs quiet and skip downloads gitframes never uses.
package.jsonskia-canvasimport "gitframes"Cannot find module '.../skia.node'For a new project, create with all three blocks. In an existing project, merge them into its , keeping any entries it already has:
package.jsonpackage.jsonjson
{
"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"]
}allowScriptspnpmtrustedDependenciesbash
npm install gitframes
npm install --save-dev tsx typescript @types/nodeThe install must finish without an warning. If gitframes was installed before the approval was in place, add it and rerun the skipped script with (). Never approve every script wholesale ().
install-scriptsnpm rebuild skia-canvaspnpm rebuild skia-canvasnpm install-scripts approve --allA minimal :
tsconfig.jsonjson
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext",
"target": "ES2022",
"strict": true,
"skipLibCheck": true,
"noEmit": true,
"types": ["node"]
},
"include": ["src/**/*"]
}使用项目当前采用的 package manager(查找 、、 或 )。
pnpm-lock.yamlyarn.lockbun.lockpackage-lock.json安装前先写入 。 除非项目明确批准,否则 npm 11、pnpm 10 和 bun 会阻止依赖项运行安装脚本,而 gitframes 恰好需要一个安装脚本: 会在其安装脚本中下载原生二进制文件。如果没有执行该脚本, 会因 而失败。其他附带安装脚本的 package 在没有这些脚本时也能正常工作,因此应禁用它们,以保持安装过程安静,并跳过 gitframes 永远不会使用的下载。
package.jsonskia-canvasimport "gitframes"Cannot find module '.../skia.node'对于新项目,请创建包含以下三个配置块的 。对于现有项目,请将它们合并到已有的 中,同时保留原有条目:
package.jsonpackage.jsonjson
{
"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"]
}allowScriptspnpmtrustedDependenciesbash
npm install gitframes
npm install --save-dev tsx typescript @types/node安装完成时不应出现 警告。如果安装 gitframes 时尚未批准相关脚本,请添加 并重新运行被跳过的脚本:()。切勿一次性批准所有脚本()。
install-scriptsskia-canvasnpm rebuild skia-canvaspnpm rebuild skia-canvasnpm install-scripts approve --all一个最小的 如下:
tsconfig.jsonjson
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext",
"target": "ES2022",
"strict": true,
"skipLibCheck": true,
"noEmit": true,
"types": ["node"]
},
"include": ["src/**/*"]
}3. Scaffold the project
3. 搭建项目
Keep the composition separate from the script that renders it, so tests and the render script build the same film:
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)Text needs a real font file. Ask the user which font to use, or have them drop one into ; don't download fonts on their behalf.
assets/fonts/typescript
// 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" });
}typescript
// 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;
}typescript
// 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);Add scripts to :
package.jsonjson
"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"
}让 Composition 与渲染它的脚本彼此分离,这样测试和渲染脚本构建的就是同一部影片:
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/typescript
// 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" });
}typescript
// 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;
}typescript
// 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);在 中添加 scripts:
package.jsonjson
"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"
}4. Verify, cheapest step first
4. 验证:优先执行成本最低的步骤
Never go straight to a full video render. Each step catches problems in seconds that a video render takes minutes to show:
npm run typecheck- and look at the PNGs in
npm run frames -- 0 30 60. A black frame usually means a missing font, a layer outside the canvas, or opacity stuck at 0.output/ - and check the motion reads smoothly across the contact sheet.
npm run grid - when a person is watching: they play the film with sound before you export (see below).
npm run preview - once frames and motion look right.
npm run render
Show the user the frame PNGs or the grid before the full render. covers pixel-probe tests, render QA (), and diagnosing blank frames.
gitframes-renderrenderVideo({ qa })切勿直接开始完整视频渲染。完整渲染需要数分钟才能显现的问题,以下每一步只需几秒就能发现:
npm run typecheck- ,然后查看
npm run frames -- 0 30 60中的 PNG。出现黑帧通常意味着缺少字体、图层位于画布之外,或不透明度被固定为 0。output/ - ,检查帧网格中的动画衔接是否流畅。
npm run grid - 当有人在场观看时运行 :导出前先播放带声音的影片(见下文)。
npm run preview - 当帧和动画效果确认无误后,再运行 。
npm run render
完整渲染前,向用户展示帧 PNG 或帧网格。 涵盖像素探针测试、渲染 QA()以及空白帧诊断。
gitframes-renderrenderVideo({ qa })Live preview
实时预览
npm run previewfilm.ts- Run it as a background command; it serves until its tab closes.
- It doesn't open anything. If your app has a built-in browser (Claude Code, Codex), open the URL there; otherwise give the user the link. opens their default browser.
startPreview(..., { open: true }) - Reference assets from the file's URL. The page runs itself, so asset paths must resolve in the browser too: build them from the module's own URL,
film.ts, rather thanpath.resolve(path.dirname(fileURLToPath(import.meta.url)), "../assets/fonts")or a bare relative path. The preview serves any file inside the project (the folder withprocess.cwd()); passpackage.json(one or more paths) torootfor fonts or media kept elsewhere.startPreview - After changing the film, run it again. It takes over the same URL, and the open tab reloads by itself.
- Take feedback in chat. has the details.
gitframes-render
npm run previewfilm.ts- 将其作为后台命令运行;它会持续提供预览,直到页面标签关闭。
- 它不会自动打开任何内容。如果你的应用内置浏览器(Claude Code、Codex),请在其中打开 URL;否则将链接提供给用户。会打开用户的默认浏览器。
startPreview(..., { open: true }) - 从文件自身的 URL 引用资源。 页面会自行运行 ,因此资源路径也必须能在 browser 中解析:应基于 module 自身的 URL 构建路径,例如
film.ts,不要使用path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../assets/fonts")或裸相对路径。预览服务会提供项目目录(即包含process.cwd()的文件夹)内的任何文件;如果字体或媒体文件存放在其他位置,请将相应路径传给package.json的startPreview参数。root - 修改影片后,再次运行预览。它会占用同一个 URL,已打开的标签会自动刷新。
- 在聊天中收集反馈。更多细节见 。
gitframes-render
5. Grow the film
5. 扩展影片
Once the first render works, move to the specialist skills: for splitting the film into scene modules on a beat grid, for grading, grain, and vision-driven effects, and for tests and delivery checks. Keep as the single source of sizes, colors, fonts, and timing as the project grows.
gitframes-composegitframes-effectsgitframes-rendertheme.ts首次渲染成功后,转入相应的专业 skill:使用 按节拍网格将影片拆分为场景 module,使用 进行调色、颗粒效果和视觉驱动的特效,使用 进行测试和交付检查。随着项目扩展,继续将 作为尺寸、颜色、字体和时序的唯一来源。
gitframes-composegitframes-effectsgitframes-rendertheme.ts