framefields-compose

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

framefields-compose

framefields-compose

Setup

安装设置

Import everything from the
framefields
npm package. If the project doesn't have framefields set up yet, follow the
framefields
skill first (install, project scaffold, first render).
从
framefields
npm 包中导入所有内容。如果项目尚未配置 framefields,请先按照
framefields
技能操作(安装、项目脚手架搭建、首次渲染)。

Overview

概述

framefields
is a code-first, deterministic video composition SDK executing natively on WebGPU (Dawn) in Node.js. It replaces browser-based rendering (Puppeteer/Chromium) with high-performance GPU pipelines, Yoga flexbox layout, Slug vector typography, 3D camera transforms, and reactive audio/signal drivers.
Every composition is defined as a tree of layout nodes compiled into a deterministic execution program:
(composition, frame) → pixels
.

framefields
是一款代码优先的确定性视频合成 SDK,在 Node.js 中基于 WebGPU (Dawn) 原生运行。它用高性能 GPU 管线、Yoga 弹性盒布局、Slug 矢量排版、3D 相机变换和响应式音频/信号驱动,替代了基于浏览器的渲染(Puppeteer/Chromium)。
每个合成作品都被定义为一棵布局节点树,会被编译为确定性执行程序:
(composition, frame) → pixels
。

Architecture & Core Concepts

架构与核心概念

1. Composition Lifecycle

1. 合成生命周期

A
Composition
instance defines canvas dimensions, frame rate, background, registered fonts, global audio tracks, and the layer hierarchy:
typescript
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 }));
一个
Composition
实例定义了画布尺寸、帧率、背景、已注册字体、全局音轨和图层层级:
typescript
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. Scene Architecture (Production Pattern)

2. 场景架构(生产模式)

For commercials, films, and multi-shot videos, break the project into discrete, modular scene functions using scene-local frames:
  • Tokens first (
    theme.ts
    ):
    Centralize color palette, typography, easing curves, and a tempo-locked beat grid (
    BEAT = Math.round(fps * 60 / BPM)
    ). Never use arbitrary margins or ad-hoc hex codes.
  • Scene modules (
    scenes/hero.ts
    ):
    Each scene returns a full-frame root box with
    startFrame
    and
    durationFrames
    . Children position themselves relative to scene start (
    frame: 0
    = scene onset).
  • Master assembly (
    film.ts
    ):
    Imports scenes, mounts them to
    comp.add(scene())
    , attaches the master soundtrack and composition-level grading/grain.
typescript
// 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,
  });
}

对于商业广告、电影和多镜头视频,可使用场景局部帧将项目拆分为离散的模块化场景函数:
  • 令牌优先(
    theme.ts
    ):
    集中管理调色板、排版、缓动曲线和与节奏锁定的节拍网格(
    BEAT = Math.round(fps * 60 / BPM)
    )。切勿使用随意的边距或临时的十六进制颜色代码。
  • 场景模块(
    scenes/hero.ts
    ):
    每个场景返回一个带
    startFrame
    和
    durationFrames
    的全帧根容器。子元素相对于场景起始位置定位(
    frame: 0
    = 场景开始)。
  • 主装配(
    film.ts
    ):
    导入场景,将其挂载到
    comp.add(scene())
    ,附加主音轨和合成级别的调色/颗粒效果。
typescript
// 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 Primitives (
Layer.*
)

图层基元(
Layer.*
)

All
Layer.*
builders return
AnimatableNode<T>
instances with chainable
.animate()
,
.apply()
, and effect methods.
BuilderNode
kind
Description & Key Options
Layer.text(content, options)
"text"
Slug GPU vector text. Options:
fontSize
,
fontFamily
,
fill
,
letterSpacing
,
lineHeight
,
align
,
animators
,
spans
.
Layer.box(options)
"box"
Rectangular container / card. Options:
background
,
borderRadius
,
borderColor
,
borderWidth
,
overflow: "hidden"
,
padding
,
children
.
Layer.flex(options)
"flex"
Auto-layout container (Yoga). Options:
dir: "row" | "column"
,
gap
,
justify
,
align
,
padding
,
wrap
,
children
.
Layer.video(src, options)
"media"
Video layer decoded via mediabunny. Options:
fit: "cover" | "contain"
,
trimStartSec
,
muted
,
volume
.
Layer.image(src, options)
"media"
Static raster image (
.png
,
.jpg
,
.webp
). Options:
fit: "cover" | "contain"
.
Layer.svg(src, options)
"media"
Vector SVG asset. Options:
fit: "contain"
.
Layer.lottie(src, options)
"media"
Lottie vector animation file.
Layer.audio(src, options)
"media"
Audio track layer. Options:
volume
,
startFrame
,
durationFrames
.
Layer.shape(type, options)
"shape"
Parametric vector graphics (
"rect"
,
"circle"
,
"ellipse"
,
"polygon"
,
"star"
,
"arrow"
,
"path"
). Options:
d
(SVG path data with arcs),
strokeWidth
,
strokeColor
,
trimStart
,
trimEnd
.
Layer.caption(src, options)
"media"
SRT-driven synchronized subtitle engine. Bottom-anchored auto-wrapping cues.
Layer.chart(options, props)
"box"
Charts built from native nodes (d3 scales and geometry).
type
:
"line" | "area" | "bar" | "scatter" | "candlestick" | "pie" | "donut"
. Options:
width
,
height
,
series: [{ name, data, color, type, curve, pointRadius }]
or
data
,
categories
,
stacked
,
xAxis
/
yAxis
(
min
,
max
,
ticks
,
grid
,
format
: a d3-format string like
"$,.0f"
),
legend
,
valueLabels
,
fontFamily
,
fontSize
,
colors
,
animate: { start, duration, stagger, ease } | false
.
props
positions the chart box. Children get ids
<id>-bar-<series>-<i>
,
<id>-line-<series>
,
<id>-slice-<i>
. Keep scatter charts to a few hundred points: each point is a node.
Layer.camera(options)
"camera"
3D scene camera with FOV, lens rack focus, and orbital coordinates.
Layer.light(options)
"light"
3D lighting primitive (
ambient
,
directional
,
point
,
spot
).
Layer.model(options)
"model3d"
3D mesh loader (
.gltf
,
.glb
,
.obj
,
.fbx
,
.stl
,
.ply
). Supports skinning and audio deformation compute shaders.

所有
Layer.*
构建器都返回
AnimatableNode<T>
实例,支持链式调用
.animate()
、
.apply()
和效果方法。
构建器节点
kind
说明与关键选项
Layer.text(content, options)
"text"
Slug GPU 矢量文本。选项:
fontSize
、
fontFamily
、
fill
、
letterSpacing
、
lineHeight
、
align
、
animators
、
spans
。
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"
。
Layer.svg(src, options)
"media"
矢量 SVG 资源。选项:
fit: "contain"
。
Layer.lottie(src, options)
"media"
Lottie 矢量动画文件。
Layer.audio(src, options)
"media"
音轨图层。选项:
volume
、
startFrame
、
durationFrames
。
Layer.shape(type, options)
"shape"
参数化矢量图形(
"rect"
、
"circle"
、
"ellipse"
、
"polygon"
、
"star"
、
"arrow"
、
"path"
)。选项:
d
(带弧线的 SVG 路径数据)、
strokeWidth
、
strokeColor
、
trimStart
、
trimEnd
。
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
:d3-format 字符串,如
"$,.0f"
)、
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
)。
Layer.model(options)
"model3d"
3D 网格加载器(
.gltf
、
.glb
、
.obj
、
.fbx
、
.stl
、
.ply
)。支持蒙皮和音频变形计算着色器。

Animation System (
LayerAnimation
)

动画系统(
LayerAnimation
)

Create animations using
LayerAnimation.create()
or fluent factory methods. Animations are composed of keyframed tracks or procedural drivers.
使用
LayerAnimation.create()
或流式工厂方法创建动画。动画由关键帧轨道或过程驱动组成。

1. Keyframe Transitions (
fromTo
)

1. 关键帧过渡(
fromTo
)

typescript
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")
For more than two keyframes on one prop, list them with
keys
: each tuple is
[frame, value, ease?]
, where the ease shapes the motion into that keyframe. Frames are rounded, so beat math (
at + BEAT / 2
) can go straight in:
typescript
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"]])
Keyframes past the layer's
durationFrames
never play; the render warns (
animation_truncated
) and
renderVideo({ qa: true })
reports it.
typescript
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?]
,其中缓动定义了进入该关键帧的运动曲线。帧号会被取整,因此节拍计算(
at + BEAT / 2
)可以直接传入:
typescript
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. Kinetic Typography (
TextAnimator
)

2. 动态排版(
TextAnimator
)

Text animators operate at the GPU glyph instancing level, cascading offsets across characters, words, or lines:
typescript
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")
);
Presets available:
TextAnimator.waveRise()
,
TextAnimator.wordPop()
,
TextAnimator.blurIn()
,
TextAnimator.flip3D()
,
TextAnimator.colorSweep()
.
文本动画器在 GPU 字形实例化层面运行,在字符、单词或行之间级联偏移:
typescript
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 Spatial Transforms & Camera Motion

3. 3D 空间变换与相机运动

Layers can be transformed in 3D perspective space:
typescript
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" })
);
For full 3D camera staging, use
CameraAnimation
:
typescript
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
  });
图层可在 3D 透视空间中变换:
typescript
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
:
typescript
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. Procedural & Audio-Reactive Drivers

4. 过程式与音频响应驱动

Properties can be bound to Perlin wiggles, harmonic springs, or audio-extracted signals:
typescript
// 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()
drive the prop with
value × multiplier + offset
every frame. A signal object is carried on its track and registered with the program automatically (under a generated
inline_signal_N
id, so the spec stays plain JSON); a string is looked up in
comp.addSignal
names. Anything else (a number,
undefined
) throws. Signals are sampled at the composition frame (
ctx.frame
), not the layer's local frame.

属性可绑定到 Perlin 抖动、谐波弹簧或音频提取的信号:
typescript
// 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()
每帧用
值 × 乘数 + 偏移量
驱动属性。信号对象会被承载在其轨道上,并自动注册到程序中(在生成的
inline_signal_N
ID 下,因此规范保持纯 JSON 格式);字符串会在
comp.addSignal
的名称中查找。其他任何值(数字、
undefined
)都会抛出错误。信号在合成帧(
ctx.frame
)上采样,而不是图层的局部帧。

Critical Authoring Rules & Gotchas

关键创作规则与常见坑点

  1. Local Keyframe Coordinates: Keyframes are strictly relative to the node's own timeline window (
    0
    = node appearance). Never offset keyframe frames by the parent's
    startFrame
    .
  2. Coordinate Overrides: Animating
    y
    overrides the node's layout
    y
    . If a node is placed at
    y: 400
    , animate
    fromTo("y", 440, 400)
    , NOT
    fromTo("y", 40, 0)
    .
  3. No String Styles for Borders: Use
    borderColor
    and
    borderWidth
    . CSS
    border: "1px solid red"
    is silently ignored.
  4. Font Registration is Mandatory: In headless WebGPU, system fonts do not exist. Every font must be registered via
    FontManager.register()
    and listed in
    comp.fonts
    . Emoji need a color emoji font too: framefields ships none, so register one with
    FontManager.registerEmojiFont("assets/fonts/NotoColorEmoji.ttf")
    (a CBDT/CBLC bitmap font such as Noto Color Emoji). Without it exports draw nothing for emoji.
  5. Integer Frames Only: Keyframe frames, start frames, and durations must be integers. Decimal frames cause schema validation errors.
  6. Relative vs. Absolute: Providing
    x
    or
    y
    without
    position: "relative"
    turns the node into an absolute-positioned layer removed from flex flow.
  7. Audio Layer Duration: Every
    Layer.audio()
    must be assigned
    durationFrames
    to prevent audio truncation in headless rendering.
  8. Premultiplied Alpha: Alpha shaders operate with premultiplied alpha. Never use
    "src-alpha"
    factor in custom blends.
  1. 局部关键帧坐标: 关键帧严格相对于节点自身的时间线窗口(
    0
    = 节点出现)。切勿用父级的
    startFrame
    偏移关键帧的帧号。
  2. 坐标覆盖: 对
    y
    做动画会覆盖节点的布局
    y
    。如果节点的位置是
    y: 400
    ,动画应写
    fromTo("y", 440, 400)
    ,而不是
    fromTo("y", 40, 0)
    。
  3. 边框不支持字符串样式: 请使用
    borderColor
    和
    borderWidth
    。CSS 的
    border: "1px solid red"
    会被静默忽略。
  4. 字体注册是强制性的: 在无头 WebGPU 中,不存在系统字体。每个字体都必须通过
    FontManager.register()
    注册,并列入
    comp.fonts
    。表情符号也需要彩色表情字体:framefields 不附带任何表情字体,因此请使用
    FontManager.registerEmojiFont("assets/fonts/NotoColorEmoji.ttf")
    注册一个(CBDT/CBLC 位图字体,如 Noto Color Emoji)。没有它的话,导出时表情符号不会显示任何内容。
  5. 仅支持整数帧: 关键帧帧号、起始帧和持续时间必须是整数。小数帧会导致模式验证错误。
  6. 相对与绝对: 如果提供了
    x
    或
    y
    但没有设置
    position: "relative"
    ,节点会变成绝对定位图层,从弹性流中移除。
  7. 音频图层持续时间: 每个
    Layer.audio()
    都必须指定
    durationFrames
    ,以防止无头渲染中的音频被截断。
  8. 预乘 Alpha: Alpha 着色器使用预乘 Alpha 运行。切勿在自定义混合中使用
    "src-alpha"
    因子。