23 KiB
UI 元素
本节将会讲解 UI 系统中常用的渲染元素以及基础使用。
通用属性
UI 元素包含很多通用属性,我们先来介绍这些属性,它们可以用在任何渲染元素和 UI 组件中。
定位属性
元素包含若干定位属性,其中最常用的是 loc
属性,我们也推荐全部使用这个属性来修改元素定位。其类型声明如下:
type ElementLocator = [
x?: number,
y?: number,
w?: number,
h?: number,
ax?: number,
ay?: number
];
这些属性两两组成一组(x, y
一组,w, h
一组,ax, ay
一组),每组可选填,也就是说 x
和 y
要么都填,要么都不填,以此类推。
x
y
: 元素的位置,描述了在没有旋转时元素的锚点位置,例如[32, 32]
就表示这个元素锚点在32, 32
的位置,默认锚点在元素左上角,也就表示元素左上角在32, 32
。w
h
: 元素的长宽,描述了在没有缩放时元素的矩形长宽,默认是没有放缩的。ax
ay
: 元素的锚点位置,描述了元素参考点的位置,所有位置变换等将以此点作为参考点。0 表示元素最左侧或最上侧,1 表示最右侧或最下侧,可以填不在 0-1 范围内的值,例如[-1, 1]
表示锚点横坐标在元素左侧一个元素宽度的位置,纵坐标在元素下边缘的位置。
示例如下:
// 元素相对于 32, 32 位置居中(锚点在元素正中间),宽高为 64
<sprite loc={[32, 32, 64, 64, 0.5, 0.5]} />
除了 loc
属性之外,还可以通过设置 anc
属性来修改锚点位置,示例如下:
// 设置锚点,效果为靠右对齐,上下居中对齐
<sprite anc={[1, 0.5]} />
你还可以手动指定 x
y
width
height
anchorX
anchorY
属性,但是这种方式比较啰嗦,并不建议使用:
<sprite x={32} y={32} width={64} height={64} anchorX={0.5} anchorY={0.5} />
最后说明一下元素的 type
属性,此属性描述了元素的定位模式,默认为 static
常规定位,此定位模式下元素位置会按照上述内容更改,而在 absolute
模式下,不论怎么修改定位属性,它都会保持在左上角的位置,可能会在一些特殊场景下使用(极度不建议使用此属性,很可能在 2.B.1 版本就会将其删除)。
纵深属性
可以通过 zIndex
属性来调整一个元素的纵深。纵深描述了元素之间的重叠关系,纵深高的会处在纵深低的元素上方,同时也会阻碍交互事件向纵深低的元素传播。必要的时候,需要通过设置纵深属性来调整层级关系。未设置时,后面的元素会处在前面的元素之上。
// 这个元素会处在上层
<sprite zIndex={10} />
// 这个元素会处在下层
<sprite zIndex={5} />
效果属性
效果属性包含 filter
composite
及 alpha
三个属性。
filter
表示此元素的滤镜,参考 CanvasRenderingContext2D.filter,可以填写内置函数或 svg 滤镜。默认不包含任何滤镜。示例如下:
// 亮度变为 150%,对比度变为 120%
<sprite filter="brightness(150%) contrast(120%)" />
composite
属性描述了当前元素与在此之前渲染的元素之间的混合模式,参考 CanvasRenderingContext2D.globalCompositeOperation,可以填写 26 个值。默认使用简单 alpha
混合,即 source-over
。例如:
// 使用加算方式叠加,两个颜色的 RGB 值分别相加得到最终结果
<sprite composite="lighter" />
alpha
属性描述了此元素的不透明度,1 表示完全不透明,0 表示完全透明。在叠加时,所有颜色都会乘以此不透明度后叠加。默认值是完全不透明,即 1。但需要注意的是,虽然 1 表示完全不透明,但是如果画布内容本身包含透明内容(例如一个半透明矩形),即使是 1 也会表现为透明,因为叠加时会乘以 1,不透明度不变。示例如下:
// 一个半透明元素
<sprite alpha={0.5} />
缓存属性
可以通过 cache
和 nocache
属性来指定这个元素的缓存行为,其中 nocache
表示禁用此元素的缓存机制,优先级最高,设置后必然不使用缓存。cache
表示启用此元素的缓存行为,常用于一些默认不启用缓存的元素,优先级低于 cache
。这两个元素都不能动态设置,也就是说不能使用响应式来修改其值。示例如下:
// 内部渲染内容比较简单,不需要启用缓存
<container nocache>
<text />
</container>
// 路径较为复杂,因此启用 g-path 的缓存行为
<g-path path={veryComplexPath} cache />
元素溢出行为
溢出行为是指,当其子元素超出父元素的大小时,执行的行为。例如,假如父元素大小为 200 * 200
,里面有一个子元素,大小为 100 * 100
,位于 (150, 50)
的位置,这时候子元素的一部分就会超出父元素的范围。
在本渲染系统中,所有元素的默认溢出行为是裁剪,即不会显示任何溢出内容,注意调整容器的宽高。在 nocache
模式下,由于不受到缓存的约束,溢出内容依然会显示,不过不建议利用此特性来编写 UI,因为这种行为可能会在后续的更新中修改。
隐藏元素
可以使用 hidden
属性来隐藏元素:
const hidden = ref(false);
// 一般使用一个响应式变量来控制隐藏行为,因为设置成常量没有任何意义
<sprite hidden={hidden.value} />;
交互属性
交互属性包括 cursor
和 noevent
。前者描述了鼠标覆盖在当前元素上时的指针样式,参考 CSS: cursor。示例如下:
// 鼠标放置在该元素上时使用小手样式
<sprite cursor="pointer" />
noevent
表明当前元素将不会触发任何事件,事件将会下穿至纵深更低的元素。示例如下:
// 设置为 noevent 模式
<sprite zIndex={100} noevent />
// 这样的话这个 onClick 就可以正常触发了
<sprite zIndex={10} onClick={click} />
高清与抗锯齿
包含 hd
anti
noanti
三个属性,hd
表示是否启用高清,大部分元素是默认启用的,除了几个像素风为主的元素(地图渲染等);anti
表示手动启用画布的抗锯齿行为,一般用于默认不启用抗锯齿的元素;noanti
表示手动关闭元素的抗锯齿行为,优先级高于 anti
,一般用于像素风图片展示、图标显示等,同时也有助于提高渲染性能。
// 关闭高清
<sprite hd={false} />
// 关闭抗锯齿
<sprite noanti />
// 启用地图渲染的抗锯齿
<layer anti />
元素变换属性
可以通过调整 transform
属性来修改元素的线性变换,包括平移、旋转、缩放。如果是简易的变换,可以使用 rotate
scale
属性来修改旋转、缩放,使用 loc
来修改位置:
// 旋转 90 度,横向放缩为 1.5 倍,纵向不放缩
<sprite rotate={Math.PI / 2} scale={[1.5, 1]} loc={[32, 32]} />
我们没有设置锚点属性,那么需要注意旋转后,loc
属性所标明的位置将不再是左上角的那个点,因为旋转后,原本在左上角的点将会变成右上角的点。旋转时,顺时针为正,逆时针为负。
使用上面这种方式时,没有办法指定变换的顺序。一般情况下,变换的顺序将会影响结果,例如先旋转,再放缩,和先放缩,再旋转,其结果是不同的。下面我们来讲解一下图形学中的 2D 矩阵变换的基本概念,以及如何使用 transform
属性。
Transform 矩阵变换
大部分情况下用不到此属性,此属性理解难度较大,如果不是必须使用此属性,可以不看此小节。以下矩阵变换内容由 DeepSeek R1
模型生成,并稍作修改。
为什么需要变换矩阵?
在 2D 图形学中,变换矩阵(3x3)可以统一表示以下基本变换操作:
- 平移(Translation)
- 旋转(Rotation)
- 缩放(Scale)
- 错切(Skew)
通过矩阵乘法可以将多个变换组合为单个矩阵运算,其通用数学表示为(列主序):
Transform=\begin{bmatrix} a & b & 0 \\ c & d & 0 \\ e & f & 1 \end{bmatrix}
其中:
a,d
控制缩放和旋转b,c
控制错切e,f
控制平移
变换组合原理
矩阵乘法具有结合性但不具有交换性,变换顺序会影响最终效果,矩阵按从右到左的顺序应用变换:
最终矩阵 = 平移矩阵 × 旋转矩阵 × 缩放矩阵 × 原始坐标
Transform 类核心功能
首先创建其实例:
import { Transform } from '@motajs/render';
const trans = new Transform();
之后可以链式调用来修改矩阵:
// 链式调用示例
trans
.setTranslate(100, 50)
.rotate(Math.PI / 4)
.scale(2, 1.5);
方法对比
方法类型 | 特点 | 函数 |
---|---|---|
叠加变换 | 在现有变换基础上叠加新变换 | translate rotate scale transform |
直接设置 | 覆盖当前变换参数 | setTranslate setRotate setScale setTransform |
关键方法详解
平移变换:
// 相对移动(叠加)
trans.translate(20, -10);
// 绝对定位(覆盖)
trans.setTranslate(200, 150);
旋转变换:
// 叠加旋转 45 度
trans.rotate(Math.PI / 4);
// 设置绝对旋转角度
trans.setRotate(Math.PI / 2);
缩放变换:
// X 轴放大 2 倍,Y 轴不变(叠加)
trans.scale(2, 1);
// 设置绝对缩放比例
trans.setScale(0.5, 0.8);
高级功能
矩阵操作:
// 手动设置变换矩阵
trans.setTransform(
1,
0, // 缩放部分
0,
1, // 旋转部分
100,
50 // 平移部分
);
// 矩阵相乘(组合变换)
const combined = trans.multiply(otherTransform);
坐标变换:
// 将局部坐标转换为世界坐标,即计算一个坐标经过此变换矩阵计算后的位置
const worldPos = trans.transformed(10, 20);
// 将世界坐标转换回局部坐标,即计算一个坐标经过此变换矩阵逆转换后的位置
const localPos = trans.untransformed(150, 80);
性能优化技巧
使用自动更新机制:
import { ITransformUpdatable } from '@motajs/render';
// 绑定可更新对象
class MyElement implements ITransformUpdatable {
updateTransform() {
console.log('变换已更新!');
}
}
const element = new MyElement();
trans.bind(element); // 变换修改时自动触发 updateTransform
最佳实践
推荐变换执行顺序:
- 缩放(Scale)
- 旋转(Rotate)
- 平移(Translate)
// 正确顺序示例
trans
.setScale(2)
.rotate(Math.PI / 3)
.setTranslate(100, 50);
组合复杂变换:
// 创建子变换
const childTrans = trans
.clone() // 从 trans 复制一个相同的变换出来,以防止修改原变换
.rotate(-Math.PI / 6)
.translate(30, 0);
// 应用组合变换
const finalTrans = trans.multiply(childTrans);
应用到元素
赋值给 transform
属性即可
<sprite transform={finalTrans} />
常见问题排查
-
变换不生效?
- 验证绑定的对象是否实现
updateTransform
- 检查有没有把
trans
对象赋值给元素的transform
属性
- 验证绑定的对象是否实现
-
性能问题
- 避免高频调用
setTransform
- 优先使用叠加方法代替矩阵直接操作
- 利用
clone()
复用已有变换
- 避免高频调用
sprite
标签
sprite
标签是一个允许你自定义渲染内容的标签,它新增了一个属性 render
属性,允许你传入一个函数来执行自定义渲染。函数定义如下:
type RenderFn = (canvas: MotaOffscreenCanvas2D, transform: Transform) => void;
canvas
: 要渲染至的画布,一般直接将内容渲染至这个画布上transform
: 当前元素的变换矩阵,相对于父元素,不常用
多数情况下,我们只会使用到第一个参数,MotaOffscreenCanvas2D
接口请参考 API 文档。下面是一个典型案例:
const render = (canvas: MotaOffscreenCanvas2D) => {
const { ctx, width, height } = canvas; // 获取画布上下文以及长宽
ctx.fillStyle = '#d84'; // 设置填充样式
ctx.fillRect(0, 0, width, height); // 绘制一个矩形
};
sprite
元素的应用场景并不算多,因为样板内置的各种元素已经足够丰富,此元素一般只会在一些特殊情况下或性能敏感情况下使用。
container
标签
container
表示一个容器,它可以将一系列元素作为子元素并渲染。它并没有新增任何属性。如果你想渲染子元素,请务必使用此元素包裹,除此之外的大部分元素是不能渲染子元素的。
container-custom
标签
container-custom
是另一种容器,它允许你自定义对子元素的渲染方案,对于特殊场景下有一定的作用,例如样板自带的 Scroll
组件就使用此标签实现。它新增了一个 render
参数,定义如下:
type CustomContainerRenderFn = (
canvas: MotaOffscreenCanvas2D,
children: RenderItem[],
transform: Transform
) => void;
canvas
: 要渲染至的画布,一般直接将内容渲染至这个画布上children
: 要渲染的子元素,按zIndex
升序排列transform
: 当前元素的变换矩阵,相对于父元素,不常用
典型案例如下:
const render = (
canvas: MotaOffscreenCanvas2D,
children: RenderItem[],
transform: Transform
) => {
// 顺序遍历子元素,保证纵深关系正确
children.forEach(v => {
if (v.hidden) return; // 如果元素隐藏,则不渲染
// 调用子元素的渲染函数,传入 canvas 和 transform 作为参数
v.renderContent(canvas, transform);
});
};
<container-custom render={render} />;
text
标签
text
标签用于显示文字,它会自动计算文字的宽高并设置为元素宽高,因此不要手动指定宽高,否则可能会引起位置错误。它新增了这些属性:
interface TextProps extends BaseProps {
/** 要渲染的文字 */
text?: string;
/** 文字的填充样式 */
fillStyle?: CanvasStyle;
/** 文字的描边样式 */
strokeStyle?: CanvasStyle;
/** 文字的字体 */
font?: Font;
/** 文字的描边粗细 */
strokeWidth?: number;
}
典型案例如下:
import { Font } from '@motajs/render';
<text
// 文字内容
text="这是一段文字"
// 文字定位,不要填写宽高,如果需要填写锚点,可以使用 anc 属性或宽高填 void 0
loc={[32, 32]}
// 填充样式,纯白色
fillStyle="#fff"
// 描边样式,红色
strokeStyle="#d54"
// 字体,使用大小为 24px 的默认字体
font={new Font(24)}
// 描边宽度 3px,默认为 2px
strokeWidth={3}
/>;
image
标签
image
标签允许你显示一张图片,包含一个 image
属性,传入图片对象(注意不是注册图片名称)。用例如下:
// 获取注册的图片
const img = core.material.images.images['myImage.png'];
// 显示图片
<image image={img} />;
icon
标签
icon
标签用于显示一个图标,可以包含动画帧。它有如下参数:
export interface IconProps extends BaseProps {
/** 图标 id 或数字 */
icon: AllNumbers | AllIds;
/** 显示图标的第几帧 */
frame?: number;
/** 是否开启动画,开启后 frame 参数无效 */
animate?: boolean;
}
使用案例如下:
// 显示绿史莱姆,开启动画
<icon icon="greenSlime" animate />
winskin
标签
winskin
标签允许你显示一个 rpg maker 风格的背景图(window skin),它有如下参数:
export interface WinskinProps extends BaseProps {
/** winskin 的图片 id */
image: ImageIds;
/** 边框大小 */
borderSize?: number;
}
其中边框大小默认为 32,表示上边框和下边框加起来共 32 像素,即四周边框 16 像素宽。用例如下:
// 使用 winskin.png 作为图片,四周边框宽度为 24 像素
<winskin image="winskin.png" borderSize={48} />
图形标签
本小节讲解图形相关的标签,以下内容由 DeepSeek R1
模型生成并稍作修改。
通用属性说明
所有图形元素均支持以下核心属性:
属性分类 | 关键参数 | 说明 |
---|---|---|
填充与描边 | fill stroke |
控制是否填充/描边(不同元素默认值不同) |
样式控制 | fillStyle strokeStyle |
填充和描边样式,支持颜色/渐变等(如 '#f00' ) |
线型设置 | lineWidth lineDash |
线宽、虚线模式(如 [5, 3] 表示 5 像素实线+3 像素间隙) |
高级控制 | fillRule actionStroke |
填充规则(非零/奇偶)、是否仅在描边区域响应交互 |
矩形 <g-rect>
矩形的定位直接使用 loc
即可,示例如下:
// 基础矩形,矩形默认仅填充模式,因此如果需要描边的话需要手动指定 fill 和 stroke 参数
// 注意如果仅指定 stroke 参数的话,会变为仅描边形式
<g-rect loc={[100, 100, 200, 150]} fill stroke fillStyle="#f0f" lineWidth={2} />
圆形和扇形 <g-circle>
参数如下:
interface CirclesProps {
radius: number; // 半径
start?: number; // 起始弧度(默认0)
end?: number; // 结束弧度(默认2π)
/**
* 圆属性参数,可以填 `[圆心 x 坐标,圆心 y 坐标,半径,起始角度,终止角度]`,是 x, y, radius, start, end 的简写,
* 其中半径可选,后两项要么都填,要么都不填
*/
circle?: CircleParams;
}
示例如下:
// 完整圆形
<g-circle circle={[300, 200, 10]} fillStyle="skyblue" />
// 扇形(60度到180度)
<g-circle circle={[400, 300, 40, Math.PI/3, Math.PI]} />
直线 <g-line>
核心参数:
interface LineProps {
x1: number; // 起点X
y1: number; // 起点Y
x2: number; // 终点X
y2: number; // 终点Y
/** 直线属性简写参数,可以填 `[x1, y1, x2, y2]`,都是必填 */
line: [number, number, number, number];
}
示例如下:
// 普通直线
<g-line
line={[50, 50, 200, 150]}
strokeStyle="red"
lineDash={[10, 5]} // 虚线样式
/>
// 带箭头的参考线
<g-line
// 不使用简写形式
x1={300} y1={80}
x2={450} y2={220}
lineCap="round" // 端点圆形
lineWidth={4}
/>
三次贝塞尔曲线 <g-bezier>
核心参数:
interface BezierProps {
sx: number; // 起点X
sy: number; // 起点Y
cp1x: number; // 控制点1X
cp1y: number; // 控制点1Y
cp2x: number; // 控制点2X(三次贝塞尔)
cp2y: number; // 控制点2Y
ex: number; // 终点X
ey: number; // 终点Y
/** 三次贝塞尔曲线参数简写,可以填 `[sx, sy, cp1x, cp1y, cp2x, cp2y, ex, ey]`,都是必填 */
curve: BezierParams;
}
示例如下:
// 三次贝塞尔曲线
<g-bezier
curve={[100, 100, 150, 50, 250, 200, 300, 100]}
strokeStyle="purple"
lineWidth={3}
/>;
// 动态路径
const path = computed(() => [
startX.value,
startY.value,
control1X.value,
control1Y.value,
control2X.value,
control2Y.value,
endX.value,
endY.value
]);
<g-bezier curve={path.value} />;
二次贝塞尔曲线 <g-quad>
核心参数:
interface BezierProps {
sx: number; // 起点X
sy: number; // 起点Y
cpx: number; // 控制点X
cpy: number; // 控制点Y
ex: number; // 终点X
ey: number; // 终点Y
/** 二次贝塞尔曲线参数,可以填 `[sx, sy, cpx, cpy, ex, ey]`,都是必填 */
curve: QuadParams;
}
示例如下:
// 二次贝塞尔曲线
<g-bezier
curve={[100, 100, 250, 200, 300, 100]}
strokeStyle="purple"
lineWidth={3}
/>;
// 动态路径
const path = computed(() => [
startX.value,
startY.value,
controlX.value,
controlY.value,
endX.value,
endY.value
]);
<g-bezier curve={path.value} />;
圆角矩形 <g-rectr>
圆角矩形的核心参数与 CSS 的 border-radius 类似,如下:
interface RectRProps extends GraphicPropsBase {
/**
* 圆形圆角参数,可以填 `[r1, r2, r3, r4]`,后三项可选。填写不同数量下的表现:
* - 1个:每个角都是 `r1` 半径的圆
* - 2个:左上和右下是 `r1` 半径的圆,右上和左下是 `r2` 半径的圆
* - 3个:左上是 `r1` 半径的圆,右上和左下是 `r2` 半径的圆,右下是 `r3` 半径的圆
* - 4个:左上、右上、左下、右下 分别是 `r1, r2, r3, r4` 半径的圆
*/
circle?: RectRCircleParams;
/**
* 椭圆圆角参数,可以填 `[rx1, ry1, rx2, ry2, rx3, ry3, rx4, ry4]`,
* 两两一组,后三组可选,填写不同数量下的表现:
* - 1组:每个角都是 `[rx1, ry1]` 半径的椭圆
* - 2组:左上和右下是 `[rx1, ry1]` 半径的椭圆,右上和左下是 `[rx2, ry2]` 半径的椭圆
* - 3组:左上是 `[rx1, ry1]` 半径的椭圆,右上和左下是 `[rx2, ey2]` 半径的椭圆,右下是 `[rx3, ry3]` 半径的椭圆
* - 4组:左上、右上、左下、右下 分别是 `[rx1, ry1], [rx2, ry2], [rx3, ry3], [rx4, ry4]` 半径的椭圆
*/
ellipse?: RectREllipseParams;
}
示例如下:
// 四角圆角半径都为 10 的圆角矩形
<g-rectr loc={[0, 0, 200, 200]} circle={[10]} />
// 每个角都是横向半径为 10,纵向半径为 5 的椭圆
<g-rectr loc={[0, 0, 200, 200]} ellipse={[10, 5]} />
// 左上和右下是半径为 10 的圆角,左下和右上是半径为 25 的圆角
<g-rectr loc={[0, 0, 200, 200]} circle={[10, 25]} />
自定义路径 <g-path>
核心参数:
interface PathProps {
path?: Path2D; // 自定义路径对象
}
示例:
// 创建五角星
const starPath = new Path2D();
// ...路径绘制逻辑
<g-path
path={starPath}
fill
stroke
fillStyle="orange"
strokeStyle="#c00"
lineWidth={2}
/>;
最佳实践建议
- 交互增强:
<g-rect
fill
stroke
actionStroke // 仅在描边区域响应点击
onClick={handleSelect}
/>
- 样式复用:
// 创建样式对象
const themeStyle = {
fillStyle: '#2c3e50',
strokeStyle: '#ecf0f1',
lineWidth: 2
};
<g-rect fill {...themeStyle} />
<g-circle stroke {...themeStyle} />