framefields-compose について
framefields-composeは、シーン、レイヤー、キーフレームアニメーションを使った動画制作のためのスキルです。framefieldsでシーンを組み立て、テキスト、メディア、図形、3D、アニメーションを扱えます。
主な機能
- 01シーン単位で動画を構成
- 02レイヤーでテキストとメディアを合成
- 03キーフレームと手続き型アニメーション
- 04チャートとベクター図形の作成
- 053Dモデルとライティングの追加
活用シーン
- 01複数シーンの広告動画制作
- 02ビートに同期したアニメーション制作
- 03字幕、チャート、メディア素材の合成
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ gatewai-dev/ framefields --skill framefields-composeインストール後、Claude Code で/framefields-composeを入力して読み込まれたか確認します。
詳細
framefields-compose
セットアップ
framefields npm パッケージからすべてをインポートします。プロジェクトでまだ framefields を設定していない場合は、まず framefields スキルに従ってください(インストール、プロジェクトのひな形作成、初回のレンダリング)。
概要
framefields は、Node.js で WebGPU(Dawn)上でネイティブに実行される、コードファーストで決定論的な動画合成 SDK です。ブラウザーを使用したレンダリング(Puppeteer/Chromium)を、高性能な GPU パイプライン、Yoga のフレックスボックスレイアウト、Slug のベクタータイポグラフィ、3D カメラ変換、リアクティブなオーディオ/シグナルドライバーに置き換えます。
すべての合成は、レイアウトノードのツリーとして定義され、決定論的な実行プログラムにコンパイルされます:(合成、フレーム)→ ピクセル。
アーキテクチャと主要な概念
1. 合成のライフサイクル
Composition インスタンスでは、キャンバスのサイズ、フレームレート、背景、登録済みフォント、グローバルなオーディオトラック、レイヤー階層を定義します:
import { Composition, Layer, FontManager } from "framefields";
// 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. シーンアーキテクチャ(本番向けパターン)
コマーシャル、映画、複数ショットの動画では、シーン内のフレームを使用して、プロジェクトを個別のモジュール化されたシーン関数に分割します:
- まずトークンを定義(
theme.ts): カラーパレット、タイポグラフィ、イージング曲線、テンポに同期したビートグリッド(BEAT = Math.round(fps * 60 / BPM))を一元管理します。任意のマージンや場当たり的な16進数カラーコードは使用しないでください。 - シーンモジュール(
scenes/hero.ts): 各シーンは、startFrameとdurationFramesを持つフレーム全体を覆うルートボックスを返します。子要素はシーンの開始を基準に配置します(frame: 0= シーン開始時)。 - 全体の組み立て(
film.ts): シーンをインポートし、comp.add(scene())にマウントして、マスターサウンドトラックと合成レベルのカラーグレーディング/粒子効果を適用します。
エクスポート用の Scene / Sequence ビルダーでは、シーンの時間範囲は半開区間です:[startFrame, startFrame + durationFrames)。60フレームのシーンでローカルフレーム30から始まるルートレイヤーのデフォルトの長さは30フレームです。明示的に指定したルートレイヤーの時間範囲はシーン内に収まる必要があります。開始位置が無効な場合や範囲を超える場合は、シーン ID とレイヤー ID を含むエラーがスローされます。子要素とそのキーフレームは、引き続き親を基準としたローカル位置になります。アウトゴーイングオーバーラップは次のシーンを配置し、隣接するいずれかのシーンの長さを超えることはできません。最後のシーンには配置先がないため、そのアウトゴーイングオーバーラップによってタイムラインが短縮されることはありません。calculateTimeline().totalFrames は、シーンの終了位置のうち最大の排他的フレーム番号です。
// 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.*)
すべての Layer.* ビルダーは、チェーン可能な .animate()、.apply()、エフェクトメソッドを備えた AnimatableNode<T> インスタンスを返します。
| ビルダー | ノードの kind | 説明と主なオプション |
|---|---|---|
Layer.text(content, options) | "text" | Slug GPU ベクターテキストです。オプション:fontSize、fontFamily、fill、letterSpacing、lineHeight、align、direction、animators、spans。アラビア語、ヘブライ語、ペルシア語は自動的に右から左へシェーピングおよびレイアウトされます(direction: "auto")。強制するには "rtl"/"ltr" を設定します。align: "start"/"end" は方向に従います。 |
Layer.box(options) | "box" | 長方形のコンテナー/カードです。オプション:background、borderRadius、borderColor、borderWidth、overflow: "hidden"、padding、children。 |
Layer.flex(options) | "flex" | 自動レイアウトコンテナー(Yoga)です。オプション:dir: "row" | "column"、gap、justify、align、padding、wrap、children。 |
Layer.video(src, options) | "media" | mediabunny でデコードされる動画レイヤーです。オプション:fit: "cover" | "contain"、trimStartSec、muted、volume。 |
Layer.image(src, options) | "media" | 静的なラスター画像(.png、.jpg、.webp)です。オプション:fit: "cover" | "contain"。ベクターアニメーション(.json、.lottie)には Layer.lottie を使用します。 |
Layer.svg(src, options) | "media" | ベクター SVG アセットです。オプション:fit: "contain"。 |
Layer.lottie(src, options) | "media" | Lottie ベクターアニメーションファイル(.json、.lottie)です。ThorVG でデコードおよびレンダリングされます。オプション:fit: "contain" | "cover" | "fill"、width、height。 |
Layer.audio(src, options) | "media" | オーディオトラックレイヤーです。オプション:volume、startFrame、durationFrames。 |
Layer.shape(type, options) | "shape" | パラメトリックベクターグラフィックス("rect"、"circle"、"ellipse"、"polygon"、"star"、"arrow"、"path")です。オプション:fill(色または CSS グラデーション)、d(弧を含む SVG パスデータ。キーフレームを設定して形状をモーフィングできます)、strokeWidth、strokeColor、trimStart、trimEnd。 |
Layer.particles(options) | "media" | GPU パーティクルシステムです(GPU Particles を参照)。 |
Layer.vfx(options) | "media" | プロシージャルな全フレーム VFX:煙、炎、雨、雪、オーロラなど(Procedural VFX を参照)。 |
Layer.caption(src, options) | "media" | SRT 駆動の同期字幕エンジンです。下部に固定され、自動で折り返す字幕キューを表示します。 |
Layer.chart(options, props) | "box" | ネイティブノード(d3 のスケールとジオメトリ)で構築されるチャートです。type:"line" | "area" | "bar" | "scatter" | "candlestick" | "pie" | "donut"。オプション:width、height、series: [{ name, data, color, type, curve, pointRadius }] または data、categories、stacked、xAxis/yAxis(min、max、ticks、grid、format:"$,.0f" のような d3-format 文字列)、legend、valueLabels、fontFamily、fontSize、colors、animate: { start, duration, stagger, ease } | false。props はチャートボックスの位置を指定します。子要素には <id>-bar-<series>-<i>、<id>-line-<series>、<id>-slice-<i> の ID が付きます。散布図は数百個程度のポイントに抑えてください。各ポイントがノードになります。 |
Layer.camera(options) | "camera" | FOV、レンズのラックフォーカス、軌道座標を備えた 3D シーンカメラです。 |
Layer.light(options) | "light" | 3D 照明プリミティブ(ambient、directional、point、spot)です。Directional と spot は castShadow: true(シーンごとに 1 つ)、shadowSoftness、shadowDarkness を指定できます。 |
Layer.environment(preset, options) | "light" | 3D シーンのワールド設定です。スタジオのイメージベース照明、露出、トーンマッピング、フォグ、ブルームを指定します。これを追加すると、シーンは物理ベースのシェーディングに切り替わります。 下記の 3D Look を参照してください。 |
Layer.sequence(entries, options) | "box" | Transition を挟んでシーンを連続して配置します。長さを totalFrames とするボックスを返します。Scenes & Transitions を参照してください。 |
Layer.model(options) | "model3d" | 3D メッシュローダー(.gltf、.glb、.obj、.fbx、.stl、.ply)です。スキニングとオーディオ変形用のコンピュートシェーダーに対応しています。 |
Layer3D.roundedBox / sphere / cylinder(options) | "model3d" | x/y/z を中心とするプロシージャルなソリッドメッシュです(y は下向きなので、「上」は -y です)。roundedBox:width、height、depth、radius、segments、taper(上面のスケール。キーキャップには <1 を指定します)。sphere:radius。cylinder:radius または radiusTop/radiusBottom、height、openEnded。color は組み込み済みで、tint はその色に乗算され、color プロパティとしてアニメーション化されます。表面設定:roughness、metallic、emissive、emissiveIntensity、castShadow。wireframe: true を指定すると、三角形分割や裏側のエッジも含め、メッシュの三角形の辺が 1 ピクセル幅の線で描画されます。同じ形状は 1 つの GPU バッファーを共有するため、何百個複製しても低コストです。 |
メディアの時間リマッピング
Layer.ambientLight({ color, intensity, id }) と Light.ambient({ color, intensity, id }) は、ほかのライトヘルパーと同様にオプションオブジェクトを受け取ります。位置引数の (color, intensity, options) 形式も引き続きサポートされます。レイヤーの animation オプションに渡されたアニメーションビルダーのインスタンスは、CameraAnimation を含め、プログラム仕様に正規化されます。
動画レイヤーと音声レイヤーは、明示的な durationFrames を指定する timeRemap を受け取ります。マップするフレームはレイヤー内のローカルフレームで、sourceSec はソースの絶対時間です。ソース時刻が増加すると順方向に再生し、減少すると逆再生します。同じソース時刻が続くと、映像は静止し、音声は無音になります。再生速度の変更は音声のピッチも変化させます。これを trimStartSec と併用しないでください。
const clip = Layer.video("assets/clip.mp4", {
durationFrames: 90,
timeRemap: { kind: "keyframes", keyframes: [
{ frame: 0, sourceSec: 1, interpolation: "smooth" },
{ frame: 30, sourceSec: 2 }, { frame: 45, sourceSec: 2 },
{ frame: 90, sourceSec: 1 },
] },
});
comp.add(clip);
const vision = comp.vision(clip);
// Or: timeRemap: { kind: "rate", sourceStartSec: 3, rate: -0.5 }
代わりにシグナルでクロックを駆動できます。{ kind: "signal", mode: "speed", signal, sourceStartSec } は再生速度(1秒あたりのソース秒数)を積分します。負の値では逆再生し、0では停止します。mode: "source-time" はソースの絶対時間を読み取ります。signal は Signal、またはコンポジションのシグナル名(comp.addSignal)です。コンポジションのクロック上で各レイヤーフレーム境界に読み取られ、その間は補間されるため、映像、音声、comp.vision は同じクロックを共有します。
const speed = Signal.state(1);
comp.onRequestFrame((ctx) => speed.set(ctx.frame < 48 ? 1 : 0.25));
comp.add(Layer.video("assets/clip.mp4", {
durationFrames: 96,
timeRemap: { kind: "signal", mode: "speed", signal: speed },
}));
フレームフック(onRequestFrame、onPrepareFrame、comp.vision、物理演算)がない場合、シグナルは事前にサンプリングされます。これらがある場合はライブで処理されます。各境界は、そのフレームの onRequestFrame フックの実行後に一度だけ記録され、レンダリング中のフレームより先に進むことはありません。前方へシークすると、スキップされたフレームのフックが順番に実行されます。後方へシークすると、記録済みの履歴が再利用されます。ソース範囲は各フレームの記録時にチェックされ、レイヤーがコンポジションより長く続く場合は最後の値が保持されます。自身のレイヤーの現在のソース時刻を読み取るシグナルは、循環依存として報告されます。
キーフレームはフレーム 0 で始まり、durationFrames で終わり、フレーム番号が増加する必要があります。interpolation はセグメントの開始キーに指定します。値は "linear"(デフォルト)、"ease-in"、"ease-out"、"smooth" です。これらのカーブは単調で、オーバーシュートしません。smooth は速度 0 で始まり、速度 0 で終了します。隣接するセグメントは、接続点で異なる速度を持つ場合があります。comp.vision(layer) を使うには、レイヤーがすでに追加されている必要があります。クリップ解析はソース順に実行され、マッピング後のクロックでサンプリングされます。エフェクトはコンポジションのクロック上に維持されます。ソース範囲はレンダリング前にチェックされます。音声には、ピッチを維持するタイムストレッチではなく、リサンプリングが使用されます。逆再生動画は、ソースのシークによりレンダリングが遅くなる場合があります。ヘッドレスデコードでは、デコーダーごとにキャッシュされるフレームが最大 48 MiB に制限されます。
アニメーションシステム(LayerAnimation)
LayerAnimation.create() または流れるように呼び出せるファクトリーメソッドを使ってアニメーションを作成します。アニメーションは、キーフレーム付きトラックまたはプロシージャルドライバーで構成されます。
1. キーフレーム遷移(fromTo)
import { LayerAnimation } from "framefields";
// 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")
1つのプロパティに3つ以上のキーフレームを設定するには、keys に列挙します。各タプルは [frame, value, ease?] で、ease はそのキーフレームに向かう動きを形作ります。フレームは丸められるため、ビートの計算(at + BEAT / 2)をそのまま渡せます。
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"]])
レイヤーの durationFrames を超えるキーフレームは再生されません。レンダリング時に警告(animation_truncated)が表示され、renderVideo({ qa: true }) で報告されます。
2. キネティックタイポグラフィ(TextAnimator)
テキストアニメーターは GPU のグリフインスタンス化レベルで動作し、文字、単語、行にわたってオフセットを連鎖させます。
import { Layer, LayerAnimation, TextAnimator } from "framefields";
// 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")
);
使用できるプリセット:TextAnimator.waveRise()、TextAnimator.wordPop()、TextAnimator.blurIn()、TextAnimator.flip3D()、TextAnimator.colorSweep()。
3. 3D 空間変換とカメラの動き
レイヤーは 3D 透視空間で変換できます。
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" })
);
完全な 3D カメラのステージングには、CameraAnimation を使用します。
import { CameraAnimation } from "framefields";
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. プロシージャルドライバーとオーディオリアクティブドライバー
プロパティは、パーリンノイズの揺らぎ、調和ばね、またはオーディオから抽出した信号にバインドできます。
// 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() は、毎フレーム value × multiplier + offset でプロパティを駆動します。シグナルオブジェクトはトラックに保持され、プログラムに自動登録されます(生成された inline_signal_N ID が使われるため、仕様はプレーンな JSON のままです)。文字列は comp.addSignal の名前から検索されます。それ以外(数値や undefined)を指定するとエラーが発生します。シグナルはレイヤーのローカルフレームではなく、コンポジションのフレーム(ctx.frame)でサンプリングされます。
色、グラデーション、テキストメトリクス
すべての色プロパティは、hex、rgb()、hsl()、oklch()、oklab()、CSS の色名を受け付けます。色のキーフレームは**知覚的に(OKLab で)**ブレンドされるため、赤から緑への変化は濁った色ではなく黄色を通ります。Color を使えば、手作業で計算せずに色を作成できます。すべてのヘルパーは hex 文字列を返します。
import { Color, Gradient } from "framefields";
Color.oklch(0.72, 0.16, 250); // equal L = equal perceived brightness: balanced palettes
Color.mix("#ff5a1f", "#1f6bff", 0.5); // perceptual mix
const wave = Color.ramp(["#3b82f6", "#8b5cf6", "#ec4899"]); wave(0.3);
Color.steps(["#0E0D0C", "#FF5A1F"], 5); // 5 swatches
Color.lighten(EMBER, 0.1); Color.alpha("#000", 0.4); Color.readableOn(bg);
Color.contrast(fg, bg); // WCAG ratio; ≥ 4.5 body text, ≥ 3 large
グラデーションは CSS 文字列で、任意の background(ボックス、フレックス、カード。角丸も維持されます)や任意のシェイプの fill で使用できます。線形、放射状、円錐状のグラデーションに最大 8 個のストップを指定でき、ハードストップ(red 0 50%, blue 0)や、色相を保つ transparent のフェードも使用できます。
Layer.box({ background: "linear-gradient(135deg, #ff5a1f, #ffb800 60%, #7c3aed)" });
Layer.shape("circle", { fill: Gradient.radial(["#fff", "transparent"]) });
Layer.box({ background: Gradient.conic(["red", "yellow", "lime", "aqua", "blue", "magenta", "red"]) });
(カーソルを配置したり、単語に下線を引いたり、ピルのサイズを決めたりする場合は)推測した文字送り幅ではなく、実際のフォントでテキストを計測できます。
const m = FontManager.measure("Hello, world", { fontFamily: "Inter", fontSize: 64 });
m.width; m.height; m.lines; m.caretX(5); m.glyphs[0].x; // px, relative to the text box
アニメーションの合成
.animate() は、ノードにすでに設定されているものに重ねて適用されます。複数回呼び出すか、複数のアニメーションを渡すことができます。同じプロパティのキーフレームトラックは、1 つのソート済みトラックに統合されます。同じフレームで競合する場合は、後のキーが優先されます。動作を個別に定義してから組み合わせることができます。
const rainIn = LayerAnimation.keys("y", [[0, -300], [d, -300], [d + 12, 0, "back.out(1.5)"]]);
const press = LayerAnimation.keys("y", [[40, 0], [42, 6, "power2.in"], [47, 0, "power2.out"]]);
const glow = LayerAnimation.keys("color", [[42, GLOW], [60, REST, "sine.inOut"]]);
key.animate(rainIn).animate(press, glow); // or LayerAnimation.merge(rainIn, press, glow)
ステップタイミング(「2コマ打ち」)
LayerAnimation.stepped(n) は、レイヤーのアニメーション対象のすべての値を n フレーム保持します。2 は「2コマ打ち」で、セルアニメーション、ストップモーション、切り絵のような、カクカクとした手作り感を演出します。レイヤーのすべてのトラックに適用されます(キーフレームはイージングを維持し、シグナルや揺らぎもステップします)。ステップアニメーションのキャラクターを、滑らかに動くカメラや背景と組み合わせると、定番のルックになります。
puppet.animate(walkCycle, bob, LayerAnimation.stepped(2));
シェイプのモーフィング
パスシェイプの d はアニメーション可能です。SVG パスを 2 つ以上キーフレームに設定すると、モーフィングできます。さまざまなパス(点の数、曲線、円弧、サブパスの数が異なるもの)を使えます。モーフィングでねじれが発生しないよう、パスは再サンプリングされ、位置が揃えられます。イージングは通常どおり適用されます。
Layer.shape("path", { position: "absolute", x: 0, y: 0, width: 400, height: 400, d: BLOB_A, fillColor: "#3B82F6" })
.animate(LayerAnimation.keys("d", [[0, BLOB_A], [24, STAR, "back.out(1.4)"], [48, HEART, "expo.inOut"]]));
別々のシェイプを液体のように融合させるには、それらを 1 つの SVG またはパーティクルレイヤーに描画し、Gooey(ぼかし+アルファしきい値=メタボール)を追加します。
ブレンドモード
任意のレイヤーに blendMode を設定して、下に描画された内容との合成方法を指定できます。グロー、テクスチャ、光漏れ、粒子、色かぶり、映像上の文字に使用します。
| モード | 用途 |
|---|---|
"normal"(デフォルト) | 通常の重ね合わせ。 |
"screen"、"lighter"(加算)、"color-dodge"、"lighten" | 光:グロー、フレア、光漏れ、黒背景で撮影された炎や火花の映像。 |
"multiply"、"color-burn"、"darken" | 影:紙やグランジのテクスチャ、影、紙上のインク。 |
"overlay"、"soft-light"、"hard-light" | 画像の陰影を保つコントラストテクスチャ、フィルム粒子、色かぶり。 |
"difference"、"exclusion" | 映像上の反転した文字、グリッチ風の閃光。 |
"hue"、"saturation"、"color"、"luminosity" | 単色レイヤーで映像の色を変更します("color" は色合いを加え、明るさを維持します)。 |
"mask-in" / "mask-out"、"luma-in" / "luma-out"、"source-*"、"destination-*"、"xor" | 合成演算子:このレイヤーが不透明/透明/明るい/暗い部分でベースを残します。ワイプには withMatte をおすすめします。 |
Layer.video("assets/light-leak.mp4", { position: "absolute", x: 0, y: 0, width: W, height: H, fit: "cover", blendMode: "screen", opacity: 0.8 })
Layer.box({ position: "absolute", x: 0, y: 0, width: W, height: H, background: "#ff3d7f", blendMode: "color", opacity: 0.6 }) // duotone-ish tint
トラックマット(マスク、ワイプ、リビール)
node.withMatte(matteLayer, mode) は、マットが不透明("alpha"、デフォルト)、透明("alpha-inverted")、明るい("luma")、または暗い("luma-inverted")部分にのみ node を表示します。マットは兄弟レイヤー(同じ親を持つ絶対配置のレイヤー)として配置され、node の開始時点を基準にキーフレームが設定されます。また、x/y/width/height が指定されていない場合、node のボックス全体を覆います。マット自体が描画されることはありません。3D レイヤー(またはモデル)では、マットは 3D パス内のスクリーン空間で機能するため、レイヤーは引き続き奥行きに応じて描画順が決まり、隠された部分が背後の要素を遮ることもありません。独自の z を持つマットは、他の 3D レイヤーと同様に投影されます。
// Text revealed by a sliding bar
Layer.text("LAUNCH", { id: "title", position: "absolute", x: 560, y: 480, width: 800, height: 120, fontSize: 120 })
.withMatte(
Layer.box({ position: "absolute", x: 560, y: 480, width: 800, height: 120, background: "#fff" })
.animate(LayerAnimation.keys("x", [[0, -240], [24, 560, "expo.out"]])),
);
// Video inside type: the text is the matte
Layer.video("assets/surf.mp4", { position: "absolute", x: 0, y: 0, width: W, height: H, fit: "cover" })
.withMatte(Layer.text("OCEAN", { position: "absolute", x: 0, y: 340, width: W, fontSize: 380, align: "center", fill: "#fff" }));
シーンとトランジション
Layer.sequence は、全画面のシーンを順番に配置し、各ペアをトランジションの長さだけ重ね合わせます。また、各シーンの startFrame/durationFrames を自動的に設定します(各シーンのキーフレームは、そのシーン自身のフレーム 0 を基準にします)。トランジションは、シーンにマージされる通常のキーフレームとマットです。
import { Layer, Transition } from "framefields";
const film = Layer.sequence([
{ scene: hero(), duration: beats(8) },
{ scene: features(), duration: beats(12), transition: Transition.push("left", 18) },
{ scene: proof(), duration: beats(8), transition: Transition.wipe("right", 16) },
{ scene: outro(), duration: beats(6), transition: Transition.iris(20) },
], { width: W, height: H });
comp.add(film); // film.totalFrames → composition length
| トランジション | 見た目 |
|---|---|
cut() | ハードカット |
crossfade(frames) | 古いシーンの上に新しいシーンがフェードインします |
fadeThrough(frames) | 背景色にいったんフェードし、再び戻ります |
push(dir, frames, ease) | 新しいシーンが古いシーンを押し出します |
slide(dir, frames) | 新しいシーンが古いシーンの上をスライドし、古いシーンは暗くなります |
zoom(frames) | 古いシーンが拡大しながらフェードし、新しいシーンが落ち着きます |
whip(dir, frames) | モーションブラーを伴う高速な押し出し |
wipe(dir, frames) | はっきりとした境界が横切ります(マット) |
iris(frames, { at }) | ある点から円形に開きます(マット) |
3Dの見た目:環境、マテリアル、シャドウ、ブルーム
Layer.environment を設定しないシーンは、従来の Blinn-Phong の見た目を保ちます。設定すると、3Dパス全体が物理ベースになります。roughness と metallic が Blender と同じように動作するリニアライトの GGX シェーディング、すべてを照らして映り込むプロシージャルなスタジオ環境、露出、トーンマッピング、さらにオプションでフォグとブルームを利用できます。
見栄えのよいプロダクトショットへの最短ルートは、ライティングリグです。 環境と、配置済みのキー/リム(/フィル)ライトを返します。キーライトはソフトなシャドウを落とします。
import { Lighting } from "framefields";
comp.add(Lighting.studio({ target: [W / 2, H / 2, 0] })); // also: soft, sunset, dramatic
comp.add(Lighting.dramatic({ target: [W / 2, H / 2, 0], bloom: true, fog: { color: BG, start: 2500, end: 6000 } }));
// knobs: intensity, exposure (stops), rotation (turns the rig), shadowSoftness, shadows: false
または、手動で設定します。
comp.add([
Layer.environment("studio", { // presets: studio | soft | sunset | night | neutral
exposure: 0.3, // stops
toneMapping: "neutral", // default; "agx" filmic, "aces" punchy, "none"
rotation: 30, // turn the reflections
fog: { color: BG, start: 2000, end: 5000 }, // surfaces fade into the background color
bloom: true, // or { intensity: 0.3, threshold: 1.6, radius: 6 }
}),
Layer.directionalLight({ x: 400, y: -1200, z: -600, targetX: 960, targetY: 540, castShadow: true }),
]);
Layer3D.sphere({ radius: 90, color: "#e8e8e8", metallic: 1, roughness: 0.15 }); // chrome
Layer3D.roundedBox({ ..., color: "#ff5a1f", roughness: 0.45 }); // satin plastic
Layer3D.cylinder({ ..., color: "#111", emissive: "#4da3ff", emissiveIntensity: 3 }); // neon tube
- トーンマッピング
"neutral"(デフォルト)は、指定したブランドカラーをそのまま保ちます。オレンジはオレンジのままです。フィルムのような階調のロールオフには"agx"を使用します。 - ブルームは、白を大きく超える光(emissive ≳ 3、強いスペキュラ)にのみ反応するため、通常の明るい面は発光しません。発光させるには
emissiveIntensityを上げてください。 - シャドウ: シーンごとに
castShadowを設定したディレクショナルライトまたはスポットライトを1つ使用します。メッシュはデフォルトでシャドウを落とします(無効にするにはcastShadow: false)。平面レイヤー(平面、カード)がシャドウを落とすのは、castShadow: trueの場合のみです。すべてのオブジェクトがシャドウを受けます。 - フォグは
fog.colorまで正確にフェードするため、継ぎ目のない地平線にするには、コンポジションの背景色を設定してください。 - モデル: glTF マテリアルは、ベースカラー、法線、メタリック・ラフネス、オクルージョン、エミッシブの各マップを取り込みます。OBJ(
.mtl内のmap_Kd、norm)と FBX は、モデルの隣から参照されるディフューズマップと法線マップを取り込みます。テクスチャは PNG、JPEG、WebP、GIF、TIFF、AVIF である必要があります。TGA、PSD、DDS、KTX2 は警告とともにスキップされるため、変換してください。Draco/meshopt で圧縮された glTF ファイルは読み込めません。メッシュ圧縮なしで再エクスポートしてください。頂点カラー(VOX、PLY、OFF、glTFCOLOR_0)はベースカラーに乗算されます。 - モデルの向き: ワールド座標は y 軸が下向きのため、y 軸が上向きのモデル(多くの glTF、OBJ、FBX ファイル)は、直立させるために
rotateX: 180が必要です。 emissive/emissiveIntensity/exposureは、他のプロパティと同様にアニメーションできます(.keys("emissiveIntensity", …))。- 透明な3Dメッシュ: glTF の
BLENDマテリアルと、不透明度を下げたメッシュは、不透明なシーン項目の後に描画されます。各メッシュのバウンディングボックスの中心を基準に奥から手前へ並べ替えられ、深度を書き込みません。これはオブジェクト単位の近似処理です。交差するメッシュや自身と重なり合うメッシュは、常に正しく並べ替えられるとは限りません。半透明の平面レイヤーとガラスには、別の描画経路が使われます。 - ガラス: 任意の Layer3D プリミティブ、押し出しテキスト(
Layer3D.extrudedText)、またはモデルにmaterial: "glass" | "acrylic" | "frosted"を指定すると、画面上で背後にあるもの(動画、2D レイヤー、パーティクル)が、虹色の縁取り(dispersion、デフォルトは 0.04)、屈折の強さthickness(px、デフォルトは 80)、およびior(1.5)で屈折します。colorはガラスに色を付けます(縁に近いほど濃くなります)。frostedはroughnessに応じてぼかします。ガラスに環境は不要です。スタジオの映り込みはガラス自身が描画するため、暗いグレーディングで最も映えます。ガラスのみのシーンでは、Lighting.*リグを使わないでください(環境によってガラスの見た目が薄くなります)。ガラスに表示されるのは、その前に描画された2Dコンテンツと不透明な3Dのみで、他のガラスは表示されません。
物理演算(Physics.world2d/Physics.world3d)
落下、跳ね返り、積み重なり、揺れ、衝突が必要なものに使用します。たとえば、単語やカードを重力で落とす、UIパーツを転がして山にする、バネの「揺れ」、箱の中で跳ね回るボール、ドミノ、物理演算で動く3D製品などです。正確にデザインされたポーズを正確なフレームで取る必要がある動きには、代わりにキーフレームを使用してください。
ワールドはコンポジションの構築時に一度だけ実行され(Rapier、決定論的な構築:どのマシンでも同一)、フレームごとのシグナルとして再生されるため、フレームはどの順序でもレンダリングできます。simulate() は各ボディのレイヤーにバインドします(x、y、rotation。3Dでは x/y/z、rotateX/Y/Z)。
import { Layer, LayerAnimation, Physics } from "framefields";
const world = await Physics.world2d({ fps: FPS, frames: 150 }); // gravity 2400 px/s², y down
world.walls({ width: W, height: H - 160 }); // floor + sides (ceiling: true to close)
const cards = words.map((word, i) => {
const card = Layer.box({ position: "absolute", x: 140, y: -300 - i * 260, width: 560, height: 170, rotation: -8, borderRadius: 28, background: EMBER /* … */ });
world.body(card, { restitution: 0.35, startFrame: i * 6 }); // staggered drop
return card;
});
world.simulate();
// Every body exposes signals; bind them to anything (props, effect uniforms):
glow.animate(LayerAnimation.create().signal("opacity", world.impact, { multiplier: 1.5 })); // loudest hit
stage.animate(LayerAnimation.create().signal("y", world.impact, { multiplier: 14 })); // camera shake
- ボディはレイヤーから生成されます。 数値の
x、yとサイズを使います。レイヤーのポーズ(rotation、scale、anchorX/Yを含む)が開始時のポーズになります。コライダーはレイヤーに追従します。円は球形になり、borderRadiusを指定するとボックスの角が丸くなります。ポリゴン/星形/パスは凸包を使い、サイズのないテキストは寸法を測定します。shape: "box" | "circle" | "hull"とsize: [w, h]で上書きできます。通常の{ x, y, width, height }は目に見えない障害物になります。 - ボディのオプション:
type: "dynamic" | "fixed" | "kinematic"、restitution(0.3)、friction(0.6)、density(1)、velocity: [vx, vy]px/s、angularVelocity°/s、linearDamping、angularDamping、gravityScale(0 は浮遊)、lockRotation、ccd(非常に高速なボディ用)、sensor、startFrame(そのフレームまでポーズを維持)、output: "signals" | "keyframes" | "none"。 - ボディごとのシグナル:
x、y、rotation、centerX、centerY、vx、vy、speed、angularVelocity、sleeping、impact(新しい接触のたびに上昇し、1 は約 1500 px/s の衝突に相当します。impactDecayフレームかけて減衰します)。world.impactは各フレームで最も大きな衝撃を受けたボディを示します。body.collisions/world.collisionsには最初の接触{ frame, with, speed }が一覧表示されます。これらのフレームに合わせて衝突音やフラッシュを配置できます。 - シーン:
world.ground({ y })、world.walls({ x, y, width, height, ceiling })。 - ジョイント:
world.pin(body, [x, y])(点を中心に揺動)、world.hinge(a, b, [x, y])、world.weld(a, b)、world.spring(a, bOrPoint, { frequency: 2, dampingRatio: 0.2, length })、world.rope(a, bOrPoint, { length })。点の単位はワールド座標の px です。 - シミュレーションの駆動:
world.force(body, { x, y })(px/s²、質量に依存しません)、world.impulse(body, frame, [vx, vy])(瞬間的な力)、キネマティックボディのfollow: { x, y, rotation }。いずれも数値、シグナル(音声、Signal.fromArray)、または(frame) => valueを指定できます。たとえば、衝撃に合わせて跳ねる床を作れます。 - 力場:
world.field((s) => [ax, ay], { bodies })はソルバーの各ステップで呼び出されます。ボディの中心s.x、s.y(px)、速度s.vx、s.vy、およびs.frameが渡され、px/s² または何も返しません。風、渦、引力源、フローフィールド、そして出演者(以下を参照)に使えます。 - シミュレーション内の出演者:
const dancer = comp.vision("dancer.mp4")で、クリップの各フレームのポーズとシルエットをオンデマンドで解析できます(framefields-effectsを参照)。フレームを一度にすべて処理するのではなく、描画に合わせてステップするように、ワールドをライブにします。simulate()の代わりにworld.live(comp)を使います。各フレームの描画前に、そのフレームのクリップのビジョンが計算(またはキャッシュから読み込み)され、ワールドがそのフレームまで進みます。事前解析は行われず、プレビューはすぐに再生されながら解析結果が追加され、エクスポートも同じシミュレーションと同じ順序で同一の結果になります。次に、const person = dancer.person.place({ width: W, height: H, fit: "cover" })を使います。- 物を受け止めたり、たたいたりするキネマティックコライダーとして手足を配置します。例:
world.body({ x, y, width: 2 * r, height: 2 * r }, { type: "kinematic", shape: "circle", follow: { x: (f) => person.point("leftWrist", f).x - r, y: (f) => person.point("leftWrist", f).y - r } })。肩から肘、手首に沿ってボールをいくつか配置します。 - シルエット(ドレスを含む)を力場として使います。
person.sample(frame, x, y)はalpha(内側では > 0.5)と、内側を指す勾配gx、gyを返します。そのため、world.field((s) => { const m = person.sample(s.frame, s.x, s.y); … return [-m.gx * k, -m.gy * k]; })とすると、粒子や小道具が動く人物の周囲を流れるようになります。 - 読み取りは同期的に行われるため、対象フレームの解析が必要です。ライブワールド内、レンダリング中、または
await dancer.ready(frame)の実行後に読み取ります(たとえばready(0)を呼ぶと、最初のポーズを基準に配置できます)。ライブワールドでは先読みできないため、初期のフレームを基準に物を配置します。 - 同じクリップのビジョン切り抜き(
Layer.video(src).withVision({ mode: "matte" }))を粒子の上に重ねると、粒子が出演者の背後を通るようになります。 - ライブワールドではシグナルのみをバインドします(
bakeKeyframes()は使いません)。await world.advanceTo(frame)を使うと、レンダリングせずにそのフレームまで進められます(例:collisionsを読み取る場合)。
- 物を受け止めたり、たたいたりするキネマティックコライダーとして手足を配置します。例:
- 粒子: 雲、ほこり、火花、流体、フローフィールドには、何百もの物理ボディではなく
Layer.particles(以下の GPU を使用)を使います。衝突や積み重なりが必要なもの(カード、文字、製品)に限り、物理ボディを数十個程度使います。 - シグナルの代わりにキーフレームを使う:
simulate()の代わりにworld.bakeKeyframes({ tolerance: 0.25 })を使うと、編集可能な通常の JSON として、単純化された線形キーフレームが書き出されます。 - 3D:
Physics.world3dはLayer3D.roundedBox / sphere / cylinderとモデル(メッシュの凸包)を受け取り、3 成分のベクトルで同じオプションを使えます。world.hinge(a, b, at, axis)、world.walls({ width, height, depth })にも対応します。 - タイミング: シグナルはコンポジションのクロックに基づきます。フレーム
Sから始まるシーンでは、ワールドにstartFrame: Sを渡します。その前とframesを過ぎた後は、シグナルは最初/最後の値を維持します。simulate()の前(またはシグナルを読み取る前)に、すべてのボディ、ジョイント、力を追加してください。その後、ワールドはロックされます。 - 使い心地: 重力を 2400 px/s² にすると、1080p キャンバス上で軽快な動きになります。
pixelsPerMeter(100)で、物の重さの感じ方を調整できます。高く積み重ねた物や、薄くて高速なボディには、substepsを増やします(4)。 - インストールに関する注意:物理シミュレーションでは、オプションの依存関係
@dimforge/rapier2d-deterministic-compat/rapier3d-deterministic-compatを使います(framefields と一緒にデフォルトでインストールされます)。
GPU パーティクル(Layer.particles)
コンピュートシェーダーで数万個のパーティクルをシミュレーションし、1 回の呼び出しで描画します。火花、残り火、ほこり、雪、煙、インク、フローフィールド、ライトペインティング、パフォーマーが動き回る流体などを表現できます。パーティクルレイヤーはコンポジション px 単位のボックスで、その内部の位置はレイヤー px 単位です。
comp.add(Layer.particles({
width: W, height: H, max: 20000, seed: 7,
emitter: { shape: "rect", x: 0, y: H * 0.6, width: W, height: 200, rate: 3000 }, // point | line | rect | circle | ring; radial
lifetime: [4, 6], speed: [0, 40], direction: -90, spread: 360,
gravity: 20, drag: 1.5, // px/s², per second
noise: { strength: 260, scale: 170, speed: 0.25 }, // curl-noise flow: smoke, ink, flow fields
attractors: [{ x: W / 2, y: H / 2, strength: 900, radius: 200 }], // negative strength repels
push: { person, strength: 9000, sweep: 120000 }, // a performer's silhouette pushes them (comp.vision)
floor: H - 60, bounce: 0.2, bounds: "none", // none | kill | bounce | wrap
size: [10, 4], sizeJitter: 0.5, fade: [0.1, 0.3],
shape: "circle", // circle | soft (glow) | square | spark (streaks along motion)
color: ["#ffd23f", "#ff5a1f", "#7a2e14"], // over life, or by speed with colorBySpeed: 600
blend: "normal", // add for glows and light painting
prewarm: 3, // seconds simulated before frame 0: starts full
}));
emitter.x/emitter.y、rate、gravity、drag、noise.strength、アトラクターの位置/強度、pushの強度には、数値、シグナル(オーディオ、物理演算のimpact)、または(frame) => valueを指定でき、毎フレーム読み取られます。ビートに合わせたバースト、風の吹き込み、追跡した手に追従するアトラクター(person.point("rightWrist", f))などに使えます。emitter.bursts: [{ frame, count }]を指定すると、単発の爆発を追加できます。- レシピ:ライトペインティングでは、手首の追跡位置に
x/yが追従する加算合成の"soft"ポイントエミッター((f) => person.point("rightWrist", f).x)、約 1 秒の寿命、低い速度を設定し、手の軌跡に沿って発生位置を分散させることで、速い動きでも途切れないようにします。火花/噴水では、shape: "spark"、blend: "add"、重力、floorを指定します。煙/インクでは、shape: "soft"、低い不透明度、noise、大きくなっていくサイズを指定します。雪/ほこりでは、フレーム上部にrectエミッターを置き、弱い重力、noise、bounds: "wrap"を指定します。渦巻きでは、ringエミッターにアトラクターとcolorBySpeedを組み合わせます。ダンサーの周囲の流体では、push: { person }とdrag2、穏やかなノイズを組み合わせ、パーティクルレイヤーの上にクリップのビジョンカットアウトを重ねます。 - システムはフレームのレンダリングに合わせて処理を進め、24 フレームごとにスナップショットを保存します。そのため、巻き戻すと最初から再生するのではなく、状態が復元されます。同じ GPU なら同じフレームが生成されます。
プロシージャル VFX(Layer.vfx)
1 つのシェーダーで描画する、フレーム全体を対象としたプロシージャルなルックです。エフェクトがない箇所は透明になります。フッテージの上または下に重ね、光のエフェクトには通常 "screen" を指定する blendMode を設定します。
Layer.vfx({ effectType: "Aurora", width: W, height: H, colorStart: "#2bff88", colorEnd: "#7a3cff", density: 0.7, speed: 1 })
effectType:Smoke、Fire、Rain、Sparks、Snow、Dust、Lightning、Magic、LensFlare、Embers、EnergyBeam、Aurora。共通の調整項目は、density、scale、speed、intensity、seed(数値またはシグナル)、および colorStart / colorEnd です。物理演算、パフォーマー、アトラクターに反応するパーティクルには、代わりに Layer.particles を使用してください。
重要な作成ルールと注意点
- ローカルのキーフレーム座標: キーフレームはノード独自のタイムラインウィンドウを基準とします(
0= ノードの表示開始)。キーフレームのフレームに、親のstartFrameを加算してはいけません。 - 座標の上書き:
yをアニメーションさせると、ノードのレイアウト上のyが上書きされます。ノードがy: 400に配置されている場合は、fromTo("y", 440, 400)を指定してください。fromTo("y", 40, 0)ではありません。 - ボーダーに文字列スタイルを使わない:
borderColorとborderWidthを使用してください。CSS のborder: "1px solid red"は無視されます。 - フォント登録は必須: ヘッドレス WebGPU にはシステムフォントがありません。すべてのフォントを
FontManager.register()で登録し、comp.fontsに追加する必要があります。絵文字にはカラー絵文字フォントも必要です。framefields には付属していないため、FontManager.registerEmojiFont("assets/fonts/NotoColorEmoji.ttf")で登録してください(Noto Color Emoji のような CBDT/CBLC ビットマップフォント)。登録しないと、エクスポート時に絵文字は何も描画されません。 - フレームは整数のみ: キーフレーム、開始フレーム、再生時間のフレーム数は整数でなければなりません。小数のフレームを指定すると、スキーマ検証エラーが発生します。
- 相対配置と絶対配置:
position: "relative"を指定せずにxまたはyを指定すると、ノードは絶対配置のレイヤーになり、フレックスフローから除外されます。 - オーディオレイヤーの再生時間: ヘッドレスレンダリングで音声が途切れないよう、すべての
Layer.audio()にdurationFramesを設定してください。 - プリマルチプライドアルファ: アルファシェーダーはプリマルチプライドアルファで動作します。カスタムブレンドで
"src-alpha"ファクターを使用してはいけません。 - レンダリング前にリントする:
comp.lint()は、透明に描画される色やグラデーション、コントラストの低いテキスト、キャンバス外のレイヤー、効果のないシャドウ設定、環境がない状態でのroughness/metallicを一覧表示します。renderVideo({ qa: true })にもこれらの検出結果が含まれます。 - Lottie レイヤーと画像レイヤー: Lottie のベクターアニメーションには
Layer.lottie("file.json" | "file.lottie", { width, height, fit })を使用してください。.jsonや.lottieをLayer.image()に渡してはいけません。Layer.image()はラスター形式のみをデコードするため、ベクターアニメーションを渡すとエラーまたは警告が発生します。 - AI 生成メディアはビルド手順ではなくアセット:
framefieldsのaiで作成する画像、クリップ、音楽、音声は、別のスクリプトで一度だけassets/generated/に生成し、ほかのファイルと同じように読み込みます。buildFilm()やシーン関数からaiを呼び出してはいけません。レンダリング、プレビュー、テストのたびに費用が発生し、フィルムの内容が変わってしまいます。キー、スクリプト、モデルカタログについては、framefieldsスキルのセクション 6 で説明しています。