关于 framefields-compose
framefields-compose 是用于 framefields 的技能,帮助制作由场景、图层和关键帧动画组成的视频。它提供组合式场景构建,以及文本、媒体、图形、3D 和动画原语。
主要功能
- 01按场景组织视频结构
- 02使用图层组合文本与媒体
- 03创建关键帧和程序化动画
- 04构建图表与矢量图形
- 05加入 3D 模型和灯光
适用场景
- 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 是一个代码优先、结果确定的视频合成 SDK,可在 Node.js 中原生运行于 WebGPU(Dawn)上。它以高性能 GPU 流水线、Yoga 弹性布局、Slug 矢量排版、3D 摄像机变换和响应式音频/信号驱动器,取代基于浏览器的渲染(Puppeteer/Chromium)。
每个合成都定义为一棵布局节点树,并编译成一个结果确定的执行程序:(composition, frame) → pixels。
架构与核心概念
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))。切勿使用任意边距或临时指定的十六进制颜色代码。 - 场景模块(
scenes/hero.ts): 每个场景都返回一个带有startFrame和durationFrames的全帧根框。子元素相对于场景起始位置定位(frame: 0= 场景开始)。 - 主装配(
film.ts): 导入场景,将其挂载到comp.add(scene()),并添加主音轨以及合成级别的调色/颗粒效果。
对于导出的 Scene / Sequence 构建器,场景时间窗口采用左闭右开区间:[startFrame, startFrame + durationFrames)。在 60 帧场景中,本地帧 30 开始的根图层默认持续 30 帧。显式设置的根图层时间窗口必须位于其场景范围内;起始帧无效或超出范围时,会报错并指出场景和图层 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.* 构建器都会返回 AnimatableNode<T> 实例,支持链式调用 .animate()、.apply() 和效果方法。
| 构造器 | 节点 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" | 程序化全帧视觉特效:烟雾、火焰、雨、雪、极光……(参见 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 为 <id>-bar-<series>-<i>、<id>-line-<series>、<id>-slice-<i>。散点图请控制在几百个点以内:每个点都是一个节点。 |
Layer.camera(options) | "camera" | 3D 场景摄像机,支持视场角、镜头焦点切换和轨道坐标。 |
Layer.light(options) | "light" | 3D 光照图元(ambient、directional、point、spot)。directional 和 spot 支持 castShadow: true(每个场景一个)、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 会以单像素线条绘制网格三角形边缘,包括三角剖分边和背面边。形状相同的网格共享一个 GPU 缓冲区,因此复制数百份也很轻量。 |
媒体时间重映射
Layer.ambientLight({ color, intensity, id }) 和 Light.ambient({ color, intensity, id }) 接受与其他灯光辅助方法相同的选项对象;仍支持位置参数形式 (color, intensity, options)。通过图层的 animation 选项传入的动画构建器实例会被规范化为程序规格,包括 CameraAnimation。
视频和音频图层支持 timeRemap,并需显式指定 durationFrames。映射帧为图层本地帧;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 } 会对播放速率(每秒对应的源秒数)积分;负值表示反向播放,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 的起始和结束速度为零。相邻片段在连接处可以具有不同速度。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")
若要为同一属性设置两个以上的关键帧,可用 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. 程序化与音频响应驱动器
属性可以绑定到 Perlin 摆动、谐波弹簧或从音频提取的信号:
// 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)采样,而不是按图层的本地帧采样。
颜色、渐变与文本度量
每个颜色属性都接受十六进制、rgb()、hsl()、oklch()、oklab() 和 CSS 颜色名称。颜色关键帧会进行**感知均匀(OKLab)**混合,因此红色 → 绿色会经过黄色,而不是浑浊的颜色。Color 可用于构建颜色,无需手动编写数学运算;每个辅助函数都会返回十六进制字符串:
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() 会在节点已有动画的基础上叠加动画:可以多次调用,也可以传入多个动画。同一属性上的关键帧轨道会合并为一条排序后的轨道;同一帧上的关键帧以后出现的为准。分别编写行为,再将它们组合起来:
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)
逐格节奏(“二拍”)
LayerAnimation.stepped(n) 会让图层的每个动画值保持 n 帧:2 即为“二拍”,这种卡顿感带有赛璐珞动画、定格动画和纸片剪纸的手工质感。它适用于图层的所有轨道(关键帧保留其缓动;信号和摆动也会逐格变化)。将逐格动画角色与平滑的镜头或背景搭配,即可呈现经典效果。
puppet.animate(walkCycle, bob, LayerAnimation.stepped(2));
形状变形
路径形状的 d 属性可动画化:为两个或更多 SVG 路径设置关键帧,它们就会发生变形。路径可以是任意形式(点数不同、曲线、弧线、多个子路径);系统会重新采样并对齐它们,避免变形时扭曲。缓动照常生效。
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"]]));
若要让独立形状融合成流体效果,可将它们绘制到同一个 SVG 或粒子图层中,并添加 Gooey(模糊 + Alpha 阈值 = 元球)。
混合模式
任何图层都可以使用 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。遮罩的布局与同级元素相同(同一父级;绝对定位),从节点的起始帧开始设定关键帧;如果遮罩没有 x/y/width/height,则覆盖节点的边界框。遮罩本身不会被绘制。在 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 渲染流程就会变为基于物理的渲染:采用线性光照的 GGX 着色,其中 roughness 和 metallic 的表现与 Blender 中相同;程序化摄影棚环境会为所有物体照明并产生反射;还支持曝光、色调映射,以及可选的雾效和泛光。
**打造出色产品图的最快方式:使用灯光组。**它会返回一个环境,以及布置好的主光/轮廓光(/补光),其中主光会投射柔和阴影:
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"可获得电影感的高光过渡。 - 只有远超白色的光才会产生泛光(自发光 ≳ 3、过亮的镜面反射),因此普通的亮面不会发光。提高
emissiveIntensity可让物体发光。 - **阴影:**每个场景只能有一个启用
castShadow的平行光或聚光灯。网格默认会投射阴影(设置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)。让世界保持实时,使其随帧渲染逐步模拟,而不是一次性完成:用world.live(comp)代替simulate()。每帧绘制前,都会计算该帧的视频视觉信息(或从缓存读取),然后世界推进到该帧——不会预先分析任何内容,预览会立即播放并逐步填充,导出结果也完全一致(模拟相同,顺序相同)。接着,使用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,见下文),而不要使用数百个物理刚体。物理刚体只用于少量必须碰撞和堆叠的物体(卡片、字母、产品)。 - 用关键帧代替信号: 用
world.bakeKeyframes({ tolerance: 0.25 })代替simulate(),生成简单、精简的线性关键帧(可编辑的普通 JSON)。 - 3D:
Physics.world3d接受Layer3D.roundedBox / sphere / cylinder和模型(网格的凸包);支持相同选项,但使用 3D 向量;world.hinge(a, b, at, axis)、world.walls({ width, height, depth })。 - 时间: 信号使用合成时钟。在从帧
S开始的场景中,将startFrame: S传给世界。在该帧之前以及超出frames的范围后,信号会保持首个/最后一个值。在调用simulate()(或读取任何信号)之前,添加所有刚体、关节和力;之后世界会被锁定。 - 手感: 在 1080p 画布上,2400 px/s² 的重力会带来敏捷的效果;
pixelsPerMeter(100)用于缩放物体的重量感。对于高堆叠物或细长且速度快的刚体,请提高substeps(4)。 - 安装说明:物理功能使用可选依赖
@dimforge/rapier2d-deterministic-compat/rapier3d-deterministic-compat(安装 framefields 时会默认安装)。
GPU 粒子(Layer.particles)
数万个粒子通过计算着色器模拟,并通过一次调用绘制——火花、余烬、尘埃、雪、烟雾、墨迹、流场、光绘,以及表演者穿行其中的流体。粒子层是在合成 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 }]可添加一次性爆发效果。- 配方:光绘:使用加法混合的
"soft"点发射器,其x/y跟随追踪的手腕((f) => person.point("rightWrist", f).x),生命周期约 1 s,速度较低,并沿手部轨迹分散粒子出生位置,让快速移动保持连贯;火花/喷泉:shape: "spark"、blend: "add"、重力、floor;烟雾/墨迹:shape: "soft"、低不透明度、noise、逐渐增大的尺寸;雪/尘埃:在画面上方使用rect发射器、较小的重力、noise、bounds: "wrap";旋涡:ring发射器 + 吸引器 +colorBySpeed;舞者周围的流体:push: { person }+drag2 + 轻柔的噪声,并在粒子层上方叠加片段的视觉抠像。 - 系统会随帧渲染逐步运行,并每隔 24 帧为自身创建快照,因此向后拖动时间轴时会恢复快照,而不是从头重放;相同的 GPU 会生成相同的帧。
程序化 VFX(Layer.vfx)
通过单个着色器绘制的全帧程序化效果,无效果处透明。将其叠加在素材上方或下方,并设置 blendMode(光效通常使用 "screen")。
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。CSSborder: "1px solid red"会被静默忽略。 - 必须注册字体: 在无头 WebGPU 中,系统字体不存在。每种字体都必须通过
FontManager.register()注册,并列入comp.fonts。表情符号也需要彩色表情符号字体:framefields 不附带此类字体,因此请使用FontManager.registerEmojiFont("assets/fonts/NotoColorEmoji.ttf")注册一个(例如 Noto Color Emoji 这类 CBDT/CBLC 位图字体)。否则导出时不会绘制表情符号。 - 帧数必须为整数: 关键帧、起始帧和时长都必须是整数。小数帧会导致架构验证错误。
- 相对坐标与绝对坐标: 如果提供
x或y,却未设置position: "relative",节点会变成绝对定位图层,并从弹性布局流中移除。 - 音频图层时长: 每个
Layer.audio()都必须设置durationFrames,以防无头渲染时音频被截断。 - 预乘 Alpha: Alpha 着色器使用预乘 Alpha。自定义混合中切勿使用
"src-alpha"因子。 - 渲染前先运行 lint:
comp.lint()会列出可能导致透明绘制的颜色/渐变、低对比度文本、画布外图层、无效的阴影设置,以及缺少环境时使用的roughness/metallic。renderVideo({ qa: true })会包含这些检查结果。 - Lottie 与图像图层: Lottie 矢量动画请使用
Layer.lottie("file.json" | "file.lottie", { width, height, fit })。切勿将.json或.lottie传给Layer.image();它只会解码栅格格式,传入矢量动画时会抛出异常或发出警告。 - AI 生成的媒体是资源,不是构建步骤: 使用
ai(来自framefields)生成的图像、片段、音乐或语音,应由单独的脚本生成一次并保存到assets/generated/,之后像加载其他文件一样加载。切勿在buildFilm()或场景函数中调用ai:这会产生费用,并导致影片在每次渲染、预览和测试时发生变化。framefields技能(第 6 节)介绍了密钥、脚本和模型目录。