跳转到内容

内置 JSX 元素

在末语中,所有的 UI 都使用引擎提供的自定义 JSX 元素构建。这些元素不是 HTML 元素——它们直接对应引擎底层的渲染节点。

所有元素都支持以下属性:

属性 类型 默认值 说明
label string — 节点标签(调试用,在开发工具中显示)
x number 0 X 坐标
y number 0 Y 坐标
anchor [number, number] [0, 0] 锚点位置(0~1 范围,决定坐标对齐点)
pivot [number, number] [0, 0] 旋转/缩放中心(0~1 范围)
scale number 1 整体缩放
scaleX number 1 水平缩放
scaleY number 1 垂直缩放
rotation number 0 旋转角度(角度制)
skew number 0 整体斜切
skewX number 0 水平斜切
skewY number 0 垂直斜切
visible boolean true 是否可见
tint string "#FFFFFF" 着色(CSS 颜色值)
opacity number 1 透明度(0~1)
interactive boolean false 是否响应交互事件
hitTestSelf boolean true 自身是否作为命中目标;false 时自身不参与命中判定,子节点照常
cursor MoyuCursor — 鼠标悬停时的光标样式,仅在 interactive 时生效。可用自定义指针图片,见自定义鼠标指针
zIndex number 0 同一父节点下的绘制和命中顺序,支持负数

设置 interactive={true} 后,元素可以响应以下事件:

// 鼠标事件
onClick={(e: MouseEvent) => { ... }}
onMouseDown={(e: MouseEvent) => { ... }}
onMouseUp={(e: MouseEvent) => { ... }}
onMouseMove={(e: MouseEvent) => { ... }}
onMouseEnter={(e: MouseEvent) => { ... }}
onMouseLeave={(e: MouseEvent) => { ... }}
// 键盘事件
onKeyDown={(e: KeyboardEvent) => { ... }}
onKeyUp={(e: KeyboardEvent) => { ... }}
onKeyPress={(e: KeyboardEvent) => { ... }}
// 触摸事件
onTouchStart={(e: TouchEvent) => { ... }}
onTouchMove={(e: TouchEvent) => { ... }}
onTouchEnd={(e: TouchEvent) => { ... }}
onTouchCancel={(e: TouchEvent) => { ... }}

末语使用左上角为原点的坐标系,X 轴向右,Y 轴向下。坐标以舞台尺寸为基准(通常为 1920×1080),引擎会自动处理到不同窗口大小的缩放。

同一父节点下的直接子节点按照 zIndex 从小到大绘制。较大的值后绘制,因此显示在较小值的上方;zIndex 相同时保持 JSX 中的原始顺序,靠后的节点显示在上方。

<container>
<sprite src="ui/background.png" zIndex={-1} />
<sprite src="ui/panel.png" />
<text text="置顶提示" zIndex={10} />
</container>

zIndex 只比较同一个父节点的直接子节点。每个子节点会连同整棵子树一起排序,子树内部的节点不能越过父节点的兄弟节点。如果需要让一个弹出层覆盖组件外部的内容,应在两者共同父节点下,为包含弹出层的直接子节点设置 zIndex。

鼠标、触摸和滚轮事件使用相反的顺序进行命中检测,画面上最靠前的节点会优先收到事件。zIndex 不改变 JSX 顺序、布局位置或布局尺寸。


容器是最基础的布局元素,本身不渲染任何内容,用于组织和分组子元素。

<container label="角色层" x={0} y={0}>
<sprite src="characters/alice.png" />
<sprite src="characters/bob.png" />
</container>

容器支持所有通用属性。对容器设置的变换(位移、缩放、旋转等)会影响所有子元素。


按照从上到下的顺序自动排列直接子元素。适合菜单、设置项和纵向按钮组。

<vbox x={100} y={100} gap={24} padding={20}>
<text text="开始游戏" fontSize={32} />
<text text="读取存档" fontSize={32} />
<text text="退出游戏" fontSize={32} />
</vbox>

按照从左到右的顺序自动排列直接子元素。适合工具栏、横向按钮组和并排信息。

<hbox x={100} y={100} gap={16} alignItems="center">
<sprite src="ui/icon.png" />
<text text="设置" fontSize={32} />
</hbox>

<vbox> 和 <hbox> 使用相同的布局属性:

属性 类型 默认值 说明
width number 自动 显式宽度;省略时根据子元素和内边距计算
height number 自动 显式高度;省略时根据子元素和内边距计算
gap number 0 相邻子元素之间的间距
padding number 0 四边内边距
paddingX number — 水平内边距,覆盖 padding 的左右值
paddingY number — 垂直内边距,覆盖 padding 的上下值
justifyContent "start" | "center" | "end" | "space-between" "start" 子元素在排列方向上的对齐方式
alignItems "start" | "center" | "end" "start" 子元素在另一方向上的对齐方式
onLayout (event: LayoutEvent) => void — 布局尺寸变化时触发,事件包含 width 和 height

<vbox> 和 <hbox> 可以互相嵌套,也可以放进普通 <container>。完整的尺寸、对齐和嵌套规则见布局系统。


用于显示图片。这是最常用的元素之一。

<sprite src="bg/classroom.png" />
<sprite src="ui/button.png" x={100} y={200} />
属性 类型 默认值 说明
src string — 图片路径(相对于 assets/)
mode "normal" | "nineslice" "normal" 渲染模式
area [number, number, number, number] — 裁剪区域(归一化坐标 0~1:[x0, y0, x1, y1])
alphaHitTest boolean false 按纹素透明度判定命中,透明像素不参与命中(仅 mode="normal")

九宫格模式用于制作可拉伸的 UI 元素(如按钮、面板背景),保持四个角不变形:

<sprite
src="ui/panel_bg.png"
mode="nineslice"
bounds={[0.25, 0.25, 0.25, 0.25]}
targetWidth={400}
targetHeight={300}
/>
属性 类型 说明
bounds [left, top, right, bottom] 九宫格边距,相对 area 的比例(0~1)
nineSliceMode "stretch" | "repeat" | "mirror" | "blank" 边缘与中心的填充方式:拉伸、平铺、镜像平铺、中心留空
targetWidth number 目标宽度
targetHeight number 目标高度

repeat 与 mirror 按图案原始尺寸平铺,充满边缘与中心区域;blank 保持四角与边缘(同 stretch),不绘制中心。


用于渲染文本内容。支持多种打印效果。

<text
text="你好,世界!"
fontSize={32}
fillColor="#FFFFFF"
x={100}
y={100}
/>
属性 类型 默认值 说明
text string "" 文本内容
fontSize number — 字号
fillColor string — 文本颜色(CSS 颜色值)
parseMarkup boolean true 是否解析 <...> 富文本标签;设为 false 时原样显示文本
printMode "instant" | "typewriter" | "printer" "instant" 打印模式
printSpeed number — 打印速度(typewriter: 字/秒,printer: 行/秒)
boxWidth number — 文本框宽度
boxHeight number — 文本框高度
lineHeight number — 行高倍数
indent number — 段首缩进(像素)
// 描边效果
<text
text="带描边的文字"
stroke={true}
strokeColor="#000000"
strokeWidth={2}
/>
// 阴影效果
<text
text="带阴影的文字"
shadow={true}
shadowColor="#000000"
shadowOffsetX={2}
shadowOffsetY={2}
shadowBlur={4}
/>
属性 类型 默认值 说明
stroke boolean false 启用描边
strokeColor string "#000000" 描边颜色
strokeWidth number 2 描边宽度
shadow boolean false 启用阴影
shadowColor string "#000000" 阴影颜色
shadowOffsetX number 0 阴影 X 偏移
shadowOffsetY number 0 阴影 Y 偏移
shadowBlur number 0 阴影模糊半径
shadowWidth number 0 阴影宽度

<text> 元素在打印过程中会触发事件:

<text
text="正在打字……"
printMode="typewriter"
printSpeed={30}
onStart={() => console.log('开始打印')}
onProgress={(progress) => console.log(`进度: ${progress}`)}
onFinish={() => console.log('打印完成')}
/>
事件 类型 说明
onStart () => void 开始打印时触发
onProgress (progress: number) => void 打印进度(0~1)
onFinish () => void 打印完成时触发

文本完成布局时会触发 onTextLayout,可取得实际尺寸与最后一个字符后的本地光标坐标:

<text
text="需要测量的文本"
onTextLayout={({ width, height, endCursorPosition }) => {
console.log(width, height, endCursorPosition);
}}
/>

富文本 <link> 区域可以通过 onInteraction 接收局部指针事件。链接必须同时声明 id 和 target;id 是回调中的识别符,应用可据此决定是否打开或跳转。

<text
text={'阅读<link id="guide" target="/guide">使用指南</link>'}
interactive
cursor="pointer"
onInteraction={({ id, kind }) => {
if (kind === 'click') {
console.log(`点击链接:${id}`);
}
}}
/>

kind 的值为 over、enter、leave、down、up 或 click。只有指针位于对应 <link> 文本的排版区域内,onInteraction 才会触发;<text> 的其他区域仍按普通节点处理。

<text> 默认使用 < 和 > 作为富文本标签符号:

<text text="这是<color=#E7931C>橙色文字</color>,<bold>加粗</bold>,<shadow color=#0008 offsetX=1 offsetY=2 blur=4>带阴影文字</shadow>。" />

所有标签、属性、转义规则、可用状态和开发中能力见富文本。需要把 <、> 当作普通字符显示时,写成 <<、>>,或设置 parseMarkup={false}。


裁剪容器,子元素只在指定范围内可见。

<clip x={100} y={100} width={400} height={300}>
<sprite src="large_image.png" />
</clip>
属性 类型 说明
width number 裁剪区域宽度
height number 裁剪区域高度

对子元素应用视觉滤镜效果。

<filter filters={[{ type: 'blur', radius: 4 }]}>
<sprite src="bg/photo.png" />
</filter>
属性 类型 说明
filters FilterKind[] 滤镜列表(可叠加多个)

FilterKind 是一个联合类型,每种滤镜的结构如下:

type 参数 说明
'blur' radius: number, continuous?: boolean 高斯模糊
'blur-perfect' radius: number 精确模糊(质量更高,性能较低)
'brightness' amount: number 亮度(1 为原始值)
'contrast' amount: number 对比度(1 为原始值)
'saturation' amount: number 饱和度(1 为原始值)
'hue-rotate' degrees: number 色相旋转(角度)
'grayscale' amount: number 灰度(0~1)
'sepia' amount: number 复古效果(0~1)
'invert' amount: number 反色(0~1)

对元素后方的已有画面内容应用滤镜,类似 CSS 的 backdrop-filter。

<backdrop
filters={[{ type: 'blur', radius: 8 }]}
width={1920}
height={1080}
/>
属性 类型 说明
filters FilterKind[] 滤镜列表,结构同 <filter>
width number 区域宽度
height number 区域高度

用于把一个或多个输入通道交给 shader 处理。它既可以承载内建的转场效果,也可以直接运行自定义 WGSL 片元着色器。

<shader
shader={{ type: 'builtin', name: 'crossfade' }}
timeControl="transition"
displayChannel={1}
/>

<shader> 通常与 <shader-slot> 配合使用,由各个 slot 提供输入纹理。完整的 props、命令、事件和 WGSL 约定见着色器 API。

默认情况下,<shader> 的布局尺寸取所有 space="normal"、且非空 slot 内容尺寸的最大值,因此可以作为 <vbox> 或 <hbox> 的普通子节点。也可以用 width / height 显式指定任一轴;未指定的轴仍按内容自动计算。


用于为 <shader> 提供输入通道。你可以把子节点渲染进某个 channel,也可以声明一个空 channel 供 shader 按需读取。

<shader>
<shader-slot channel={0}>
<sprite src="bg/day.png" />
</shader-slot>
<shader-slot channel={1}>
<sprite src="bg/night.png" />
</shader-slot>
</shader>

常见用法还包括:

  • 设置 static={true},把明确不会变化的输入当作静态纹理使用。
  • 设置 space="shader",让子节点以 slot 自身局部原点参与采样,适合遮罩规则图这类局部输入。
  • 设置 empty={true} 并提供 width / height,声明一个没有子内容的空通道。

<shader-slot> 自身始终占用 0 × 0 布局空间,不参与父级的普通布局测量。它只负责把子内容提供给 <shader>:直接子节点的 anchor 会以这个 0 × 0 空间为基准;子树更深层的节点仍按各自父节点的实际尺寸布局。

未占用的 channel 会自动使用 dummy texture 填充。详细说明见着色器 API。


播放序列帧动画(APNG 或 WebP 动画格式)。

<animation src="effects/fire.apng" />
属性 类型 说明
src string 动画文件路径(APNG 或 WebP)
area [number, number, number, number] 裁剪区域(归一化坐标)
format "apng" | "webp" 动画格式

播放视频文件,支持 VP9(WebM/MP4)和 AV1(MP4)编码格式,支持音视频同步。视频加载完成后默认自动开始播放。

<video src="video/opening.mp4" />
{/* 循环播放背景视频,静音 */}
<video src="video/bg_loop.webm" loop muted />
{/* 控制音量,不自动播放 */}
<video src="video/scene.mp4" volume={0.5} autoPlay={false} />
属性 类型 默认值 说明
src string — 视频文件路径(相对于 assets/)
loop boolean false 是否循环播放
autoPlay boolean true 加载完成后是否自动开始播放
volume number 1 音量(0~1)
muted boolean false 是否静音
属性 类型 说明
onEnded () => void 视频播放完毕时触发(仅在非循环模式下)
onStateChange (state: string) => void 播放状态变化时触发,state 为 "idle" "loading" "playing" "paused" "stopped" "ended" "error"

播放 Cubism 模型,支持动作、表情、参数与部件透明度控制。

<live2d src="live2d/Hiyori/Hiyori.model3.json" scale={0.18} look />
属性 类型 说明
src string | null 模型入口文件路径(.model3.json);null 卸载模型。必填
physics boolean 物理演算,默认 true
pose boolean 姿态映射,控制部件的显示与隐藏,默认 true
blink boolean 自动眨眼,默认 true
breath boolean 自动呼吸,默认 true
look boolean 目光跟随指针,默认 false
idle boolean 空闲时随机播放待机动作,默认 true
hitTest boolean 按下时自动做命中测试并触发 onHit,默认 false

节点的尺寸取自模型画布,通常比舞台大很多,需要配合 scale 缩放使用。

该功能需要一份闭源插件与 Cubism Core 动态库。完整的属性、命令、事件说明见 Live2D 模型。


播放 Spine 骨骼动画模型,支持多轨动画、混合过渡、皮肤与附件切换,以及点击命中。

<spine src="spine/hero/hero.json" atlas="spine/hero/hero.atlas" scale={0.8} />
属性 类型 说明
src string | null 骨架文件路径(.json 或 .skel);null 卸载模型。必填
atlas string 图集文件路径
skin string 皮肤名,留空使用默认皮肤
timeScale number 时间缩放
paused boolean 暂停推进时间
physics "none" | "pose" | "update" 物理模式
hitArea boolean 按下时自动做命中测试并触发 onHit

节点的尺寸取自模型的原始几何范围,不随动画变化,通常需要配合 scale 缩放使用。

该功能需要一份闭源插件。完整的属性、命令、事件说明见 Spine 骨骼动画。


以下是一个实际的 UI 组合示例,展示如何用这些元素构建界面:

function SimpleDialog() {
return (
<container label="对话框">
{/* 模糊背景 */}
<backdrop filters={[{ type: 'blur', radius: 6 }]} width={1920} height={1080} />
{/* 对话框面板 */}
<sprite
src="ui/dialog_bg.png"
mode="nineslice"
bounds={[0.25, 0.25, 0.25, 0.25]}
targetWidth={800}
targetHeight={400}
x={560}
y={340}
>
{/* 标题文字 */}
<text
text="提示"
fontSize={36}
fillColor="#FFFFFF"
x={40}
y={30}
/>
{/* 内容文字 */}
<text
text="确定要退出游戏吗?"
fontSize={28}
fillColor="#CCCCCC"
x={40}
y={120}
/>
</sprite>
</container>
);
}