framefields について
framefieldsは、Node.jsとTypeScriptで使うコードベースの動画SDK向け導入スキルです。環境構築から最初のレンダリング確認までを案内し、ブラウザーを使わずWebGPUでネイティブに描画します。
主な機能
- 01動画プロジェクトのひな型作成
- 02SDKとレンダースクリプトの設定
- 03レンダリング画像と動きの確認
- 04ライブプレビューの起動
- 05動画の検証と書き出し
活用シーン
- 01TypeScript動画プロジェクトの新規構築
- 02全編を書き出す前のフレーム確認
- 03プレビューで映像と音声を確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ gatewai-dev/ framefields --skill framefieldsインストール後、Claude Code で/framefieldsを入力して読み込まれたか確認します。
詳細
翻訳後に原文が更新されています。最新の内容は原文をご確認ください。
framefields
概要
framefields はコードファーストの動画 SDK です。動画は、キーフレームアニメーションを持つレイヤーのツリーである TypeScript の Composition で、ブラウザー、Puppeteer、Chromium を使わずに、Node.js 上で WebGPU(Dawn)をネイティブに使用してレンダリングされます。エンジンは framefields npm パッケージです。以下のすべては、ユーザー自身のプロジェクトで実行されます。
このスキルでは、何もない状態から、最初のレンダリングを検証するところまで進めます。それ以降の作業には、用途別のスキルを読み込んでください。
| タスク | スキル |
|---|---|
| シーン、レイアウト、タイポグラフィ、アニメーション、ビートグリッド、映像構成 | framefields-compose |
| ポストプロセス、カラーグレーディング、VFX、デバイス上のビジョン(トラッキング、切り抜き、ポーズ) | framefields-effects |
| フレームの検査、フレームグリッド、ピクセルテスト、動画のエクスポートと QA | framefields-render |
1. 環境を確認する
- Node.js 22 以降。
node --versionを実行してください。古いバージョンではインポートに失敗します。 - GPU、または CPU レンダラー。 macOS では Metal を使用します(すべての Mac と macOS VM には Metal GPU が搭載されています)。Linux では Vulkan、Windows では Direct3D 12 または Vulkan を使用します。GPU がない場合、framefields は CPU でレンダリングします。Linux(サンドボックス、CI ランナー、コンテナー)では Mesa lavapipe を使用し、
apt-get install -y libvulkan1 mesa-vulkan-driversでインストールします。Windows では、Windows に組み込まれている WARP、または lavapipe を使用します。すべての機能(テキスト、メディア、Lottie、エフェクト、3D、ビジョン)を同じようにレンダリングできますが、速度は遅くなります。Linux でのテストでは、M1 Pro の GPU より約 12 倍遅くなりました。describeGpu()はsoftware: trueを返します。フレーム、グリッド、短い動画のエクスポートには十分です。長編動画には、GPU マシンまたはレンダーワーカー(framefields-render)を使用してください。 - ES モジュールプロジェクト。 プロジェクトの
package.jsonに"type": "module"が必要です。
2. インストール
プロジェクトで既に使用されているパッケージマネージャーを使ってください(pnpm-lock.yaml、yarn.lock、bun.lock、package-lock.json を確認してください)。
インストール前に package.json を作成してください。 npm 11、pnpm 10、bun は、プロジェクトで許可されていない限り依存関係のインストールスクリプトをブロックしますが、framefields が必要とするのは 1 つだけです。skia-canvas はインストールスクリプトでネイティブバイナリをダウンロードします。これがないと、import "framefields" は Cannot find module '.../skia.node' で失敗します。インストールスクリプトを含むその他のパッケージは、スクリプトを実行しなくても動作するため、インストールを静かに保ち、framefields が使用しないダウンロードを省くために、それらは拒否します。
新しいプロジェクトでは、3 つすべてのブロックを含む package.json を作成してください。既存のプロジェクトでは、既存のエントリを維持しながら、各ブロックを package.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 はデフォルトでインストールスクリプトを実行するため、これらは必要ありません。また、インストールに使用する正確なマネージャーとバージョンを "packageManager" に追加してください(例:"[email protected]")。インストールで生成されたロックファイル(pnpm、npm、または yarn)をコミットしてください(bun のロックファイルはワーカーでレンダリングできません)。それからインストールします。
npm install framefields
npm install --save-dev tsx typescript @types/node
インストールは、install-scripts の警告なしで完了する必要があります。許可を設定する前に framefields をインストールした場合は、設定を追加し、スキップされたスクリプトを npm rebuild skia-canvas(pnpm rebuild skia-canvas)で再実行してください。すべてのスクリプトを一括で許可しないでください(npm install-scripts approve --all)。
最小限の tsconfig.json:
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext",
"target": "ES2022",
"strict": true,
"skipLibCheck": true,
"noEmit": true,
"types": ["node"]
},
"include": ["src/**/*"]
}
3. プロジェクトをひな形から作成する
コンポジションは、レンダリングするスクリプトと分けてください。これにより、テスト、プレビュー、レンダリングスクリプト、レンダーワーカーが、すべて同じ動画をビルドします。film.ts はビルドのみを行う必要があります。レンダリング、process.exit()、インポート時の書き込みは行いません。
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
footage/ # source videos (.mp4, .mov) and audio (.mp3, .wav) tracked in Git LFS
output/ # renders (add to .gitignore)
テキストには実際のフォントファイルが必要です。使用するフォントをユーザーに尋ねてください。
// src/theme.ts
import { FontManager } from "framefields";
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 "framefields";
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, framefields", {
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 "framefields";
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"
}
動画アセットと Git LFS
動画入力(Layer.video("assets/footage/clip.mp4"))、Lottie アニメーション(Layer.lottie(...))、オーディオトラック(Layer.audio(...))、LUT、カスタムフォントは Git LFS に含め、output/ 内のレンダリング出力は .gitignore に含めます。
-
Git LFS を初期化します:
git lfs install -
プロジェクトルートに
.gitattributesを設定します:assets/**/*.mp4 filter=lfs diff=lfs merge=lfs -text assets/**/*.mov filter=lfs diff=lfs merge=lfs -text assets/**/*.webm filter=lfs diff=lfs merge=lfs -text assets/**/*.mp3 filter=lfs diff=lfs merge=lfs -text assets/**/*.wav filter=lfs diff=lfs merge=lfs -text assets/**/*.cube filter=lfs diff=lfs merge=lfs -text assets/**/*.lottie filter=lfs diff=lfs merge=lfs -text assets/fonts/**/*.ttf filter=lfs diff=lfs merge=lfs -text assets/fonts/**/*.otf filter=lfs diff=lfs merge=lfs -text -
レンダリング前にポインターを実体化します: 新しくクローンした環境やコンテナー化されたエージェント環境では、LFS メディアは約 130 バイトのテキストポインターファイルとして存在します。Framefields のネイティブ WebGPU および WebCodecs デマルチプレクサーはポインターをデコードできないため、失敗するか、空白のフレームを出力します。必ずポインターが実体化されていることを確認してください:
git lfs pull -
.gitignoreの対象を絞ります: LFS でソース映像を追跡する場合、*.mp4を.gitignoreに追加しないでください。代わりに出力ディレクトリ(output/、scratch/)を無視します。
4. 検証は、最も手軽な手順から
いきなり動画全体をレンダリングしないでください。GPU のないマシンでは、すべてのレンダリングが CPU(lavapipe または WARP)で実行されるため、手軽な手順がより重要になります。各手順では、動画レンダリングで問題が判明するまでにかかる数分に対し、数秒で問題を検出できます。
npm run typechecknpm run frames -- 0 30 60を実行し、output/内の PNG を確認します。黒いフレームは通常、フォントの欠落、レイヤーがキャンバスの外にあること、または不透明度が 0 のままになっていることを意味します。npm run gridを実行し、コンタクトシート全体で動きが滑らかに見えるか確認します。- 誰かが見ている場合は、
npm run previewを実行します。エクスポート前に、音声付きで動画を再生します(詳細は後述)。 - フレームと動きに問題がないことを確認してから、
npm run renderを実行します。
全体をレンダリングする前に、フレーム PNG またはグリッドをユーザーに見せてください。framefields-render では、ピクセルプローブテスト、レンダリング QA(renderVideo({ qa }))、空白フレームの診断について説明しています。エンジン自体に問題や不具合があると思われる場合は、https://github.com/gatewai-dev/framefields にイシューを登録してください。
ライブプレビュー
npm run preview はローカルホストのプレーヤー(映像、音声、タイムライン)を起動し、その URL を出力します。ページは film.ts 自体を実行し、WebGPU でリアルタイムにレンダリングするため、ユーザーは実際の動画を確認できます。低速なデバイスでは、音声に追いつくためにフレームがスキップされます。
- バックグラウンドコマンドとして実行し、出力を確認します。ページが開いたタイミングや、ページのエラー(ビルドの失敗、フレームでの例外、フォントの欠落)が表示され、停止した理由も出力されます。最後のタブが閉じてから 5 秒後に停止します。もう一度実行してください。
- 自動で何かが開くわけではありません。アプリに組み込みブラウザー(Claude Code、Codex)がある場合は、そこで URL を開きます。それ以外の場合は、ユーザーにリンクを渡してください。
startPreview(..., { open: true })は既定のブラウザーを開きます。 - アセットはファイルの URL を基準に参照します。 ページは
film.ts自体を実行するため、アセットパスはブラウザー内でも解決できる必要があります。process.cwd()や単純な相対パスではなく、path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../assets/fonts")のようにモジュール自体の URL を基準に作成してください。プレビューではプロジェクト内(package.jsonがあるフォルダー)のファイルをすべて配信します。フォントやメディアを別の場所に保存している場合は、startPreviewにroot(1 つ以上のパス)を渡します。 - 動画を変更した後は、もう一度実行してください。同じ URL が使われ、開いているタブは同じタイミングで自動的に再読み込みされます。
#t=12.5(秒)を使うと、特定の位置にリンクできます。スクリプトを実行できるブラウザーでは、window.framefieldsPreviewにseek(seconds)、play()、pause()、state()があります。seekはフレームの描画後に解決されるため、その後にスクリーンショットを撮ってください。- ユーザーは映像にメモをピン留めできます。 一時停止してクリックする(または範囲をドラッグする)と、その位置と再生時刻にメモが追加されます。各メモはプレビューの出力に表示され、
.framefields/preview-notes/に保存されます。保存されるのはnotes.jsonと、該当箇所をマークしたフレーム画像note-<id>.jpgです。ユーザーからメモを残したと言われたら、編集前にnotes.jsonを読み、未解決の各メモの画像を確認してください。その後、ユーザーが変更を確認してメモを完了にできるよう、プレビューをもう一度実行します。詳細はframefields-renderを参照してください。
5. サーバーでレンダリングする
framefields のレンダーワーカー(Docker、RunPod の Pod、またはサーバーレス)がプッシュされたコミットをレンダリングします。LFS アセットを取得し、ロックファイルからインストールを行い、src/film.ts をインポートして buildFilm を呼び出し、結果をアップロードします。上記の手順でビルドしたプロジェクトは、次の条件を満たせば変更せずに動作します。
framefieldsが 2.2 以降で、ロックファイルがコミットされてpackage.jsonと一致し、packageManagerが固定されていること。- すべての入力が
assets/配下にあり、Git LFS で追跡されてプッシュされていること(git lfs ls-filesに一覧表示されること)。 - アセットのパスの取得に
process.cwd()ではなくimport.meta.urlを使うこと。 - レンダリング中に
node_modulesへ書き込む処理がないこと(ファイルは読み取り専用で共有されます)。
ジョブが失敗すると { code, hint, retryable } が返されます。retryable が true の場合に限り、変更せずに再送信してください。ジョブの形式とエラーコードについては、https://framefields.dev/docs/guides/render-service/ を参照してください。
6. フィルムを発展させる
最初のレンダリングが成功したら、専門スキルに移行します。framefields-compose はフィルムをビートグリッド上のシーンモジュールに分割し、framefields-effects はカラーグレーディング、グレイン、ビジョン駆動型エフェクトを扱い、framefields-render はテストと納品チェックを行います。プロジェクトの成長に合わせて、サイズ、色、フォント、タイミングの唯一の情報源として theme.ts を維持してください。