gitframes

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

gitframes

gitframes

Overview

概览

gitframes
is a code-first video SDK. A video is a TypeScript
Composition
(a tree of layers with keyframed animation) that renders natively on WebGPU (Dawn) in Node.js, with no browser, Puppeteer, or Chromium. The engine is the
gitframes
npm package; everything below runs in the user's own project.
This skill takes a project from nothing to a verified first render. For anything past that, load the specialist skill:
TaskSkill
Scenes, layout, typography, animation, beat grids, film structure
gitframes-compose
Post-processing, color grading, VFX, on-device vision (tracking, cutouts, pose)
gitframes-effects
Frame inspection, frame grids, pixel tests, video export and QA
gitframes-render

gitframes
是一个代码优先的视频 SDK。视频是一个 TypeScript
Composition
(由带有关键帧动画的图层组成的树形结构),在 Node.js 中原生基于 WebGPU (Dawn) 渲染,无需 browser、Puppeteer 或 Chromium。引擎是
gitframes
npm package;以下所有内容都在用户自己的项目中运行。
此 skill 指导项目从零搭建,直到完成并验证首次渲染。完成首次渲染后,请加载相应的专业 skill:
任务Skill
场景、布局、字体排印、动画、节拍网格、影片结构
gitframes-compose
后期处理、调色、VFX、设备端视觉处理(跟踪、抠像、姿态)
gitframes-effects
帧检查、帧网格、像素测试、视频导出和 QA
gitframes-render

1. Check the environment

1. 检查环境

  • Node.js 22 or later. Run
    node --version
    . Older versions fail at import.
  • 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
    package.json
    needs
    "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
pnpm-lock.yaml
,
yarn.lock
,
bun.lock
, or
package-lock.json
).
Write
package.json
before installing.
npm 11, pnpm 10, and bun block dependency install scripts unless the project approves them, and gitframes needs exactly one:
skia-canvas
downloads its native binary in its install script. Without it,
import "gitframes"
fails with
Cannot find module '.../skia.node'
. The other packages that ship install scripts work without them, so deny them to keep installs quiet and skip downloads gitframes never uses.
For a new project, create
package.json
with all three blocks. In an existing project, merge them into its
package.json
, keeping any entries it already has:
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"]
}
allowScripts
is read by npm,
pnpm
by pnpm, and
trustedDependencies
by bun; each tool ignores the others' fields. Yarn runs install scripts by default and needs none of them. Then install:
bash
npm install gitframes
npm install --save-dev tsx typescript @types/node
The install must finish without an
install-scripts
warning. If gitframes was installed before the approval was in place, add it and rerun the skipped script with
npm rebuild skia-canvas
(
pnpm rebuild skia-canvas
). Never approve every script wholesale (
npm install-scripts approve --all
).
A minimal
tsconfig.json
:
json
{
  "compilerOptions": {
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "target": "ES2022",
    "strict": true,
    "skipLibCheck": true,
    "noEmit": true,
    "types": ["node"]
  },
  "include": ["src/**/*"]
}
使用项目当前采用的 package manager(查找
pnpm-lock.yaml
、
yarn.lock
、
bun.lock
或
package-lock.json
)。
安装前先写入
package.json
。
除非项目明确批准,否则 npm 11、pnpm 10 和 bun 会阻止依赖项运行安装脚本,而 gitframes 恰好需要一个安装脚本:
skia-canvas
会在其安装脚本中下载原生二进制文件。如果没有执行该脚本,
import "gitframes"
会因
Cannot find module '.../skia.node'
而失败。其他附带安装脚本的 package 在没有这些脚本时也能正常工作,因此应禁用它们,以保持安装过程安静,并跳过 gitframes 永远不会使用的下载。
对于新项目,请创建包含以下三个配置块的
package.json
。对于现有项目,请将它们合并到已有的
package.json
中,同时保留原有条目:
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"]
}
allowScripts
由 npm 读取,
pnpm
配置块由 pnpm 读取,
trustedDependencies
由 bun 读取;每个工具都会忽略其他工具的配置字段。Yarn 默认运行安装脚本,因此不需要这些配置。然后执行安装:
bash
npm install gitframes
npm install --save-dev tsx typescript @types/node
安装完成时不应出现
install-scripts
警告。如果安装 gitframes 时尚未批准相关脚本,请添加
skia-canvas
并重新运行被跳过的脚本:
npm rebuild skia-canvas
(
pnpm rebuild skia-canvas
)。切勿一次性批准所有脚本(
npm install-scripts approve --all
)。
一个最小的
tsconfig.json
如下:
json
{
  "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
assets/fonts/
; don't download fonts on their behalf.
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.json
:
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"
}
让 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);
在
package.json
中添加 scripts:
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"
}

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:
  1. npm run typecheck
  2. npm run frames -- 0 30 60
    and look at the PNGs in
    output/
    . A black frame usually means a missing font, a layer outside the canvas, or opacity stuck at 0.
  3. npm run grid
    and check the motion reads smoothly across the contact sheet.
  4. npm run preview
    when a person is watching: they play the film with sound before you export (see below).
  5. npm run render
    once frames and motion look right.
Show the user the frame PNGs or the grid before the full render.
gitframes-render
covers pixel-probe tests, render QA (
renderVideo({ qa })
), and diagnosing blank frames.
切勿直接开始完整视频渲染。完整渲染需要数分钟才能显现的问题,以下每一步只需几秒就能发现:
  1. npm run typecheck
  2. npm run frames -- 0 30 60
    ,然后查看
    output/
    中的 PNG。出现黑帧通常意味着缺少字体、图层位于画布之外,或不透明度被固定为 0。
  3. npm run grid
    ,检查帧网格中的动画衔接是否流畅。
  4. 当有人在场观看时运行
    npm run preview
    :导出前先播放带声音的影片(见下文)。
  5. 当帧和动画效果确认无误后,再运行
    npm run render
    。
完整渲染前,向用户展示帧 PNG 或帧网格。
gitframes-render
涵盖像素探针测试、渲染 QA(
renderVideo({ qa })
)以及空白帧诊断。

Live preview

实时预览

npm run preview
serves a localhost player (picture, sound, timeline) and prints its URL. The page runs
film.ts
itself and renders with WebGPU, so the user sees the real film at full frame rate.
  • 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.
    startPreview(..., { open: true })
    opens their default browser.
  • Reference assets from the file's URL. The page runs
    film.ts
    itself, so asset paths must resolve in the browser too: build them from the module's own URL,
    path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../assets/fonts")
    , rather than
    process.cwd()
    or a bare relative path. The preview serves any file inside the project (the folder with
    package.json
    ); pass
    root
    (one or more paths) to
    startPreview
    for fonts or media kept elsewhere.
  • After changing the film, run it again. It takes over the same URL, and the open tab reloads by itself.
  • Take feedback in chat.
    gitframes-render
    has the details.
npm run preview
会启动一个 localhost 播放器(画面、声音、时间线)并打印其 URL。页面会自行运行
film.ts
并使用 WebGPU 渲染,因此用户可以看到完整帧率下的真实影片。
  • 将其作为后台命令运行;它会持续提供预览,直到页面标签关闭。
  • 它不会自动打开任何内容。如果你的应用内置浏览器(Claude Code、Codex),请在其中打开 URL;否则将链接提供给用户。
    startPreview(..., { open: true })
    会打开用户的默认浏览器。
  • 从文件自身的 URL 引用资源。 页面会自行运行
    film.ts
    ,因此资源路径也必须能在 browser 中解析:应基于 module 自身的 URL 构建路径,例如
    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:
gitframes-compose
for splitting the film into scene modules on a beat grid,
gitframes-effects
for grading, grain, and vision-driven effects, and
gitframes-render
for tests and delivery checks. Keep
theme.ts
as the single source of sizes, colors, fonts, and timing as the project grows.
首次渲染成功后,转入相应的专业 skill:使用
gitframes-compose
按节拍网格将影片拆分为场景 module,使用
gitframes-effects
进行调色、颗粒效果和视觉驱动的特效,使用
gitframes-render
进行测试和交付检查。随着项目扩展,继续将
theme.ts
作为尺寸、颜色、字体和时序的唯一来源。