HumanBreak/docs/guide/ui-elements.md
2025-03-13 19:38:39 +08:00

23 KiB
Raw Blame History

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 一组),每组可选填,也就是说 xy 要么都填,要么都不填,以此类推。

  • 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 compositealpha 三个属性。

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} />

缓存属性

可以通过 cachenocache 属性来指定这个元素的缓存行为,其中 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} />;

交互属性

交互属性包括 cursornoevent。前者描述了鼠标覆盖在当前元素上时的指针样式,参考 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

最佳实践

推荐变换执行顺序:

  1. 缩放Scale
  2. 旋转Rotate
  3. 平移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} />

常见问题排查

  1. 变换不生效?

    • 验证绑定的对象是否实现 updateTransform
    • 检查有没有把 trans 对象赋值给元素的 transform 属性
  2. 性能问题

    • 避免高频调用 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}
/>;

最佳实践建议

  1. 交互增强:
<g-rect
    fill
    stroke
    actionStroke // 仅在描边区域响应点击
    onClick={handleSelect}
/>
  1. 样式复用:
// 创建样式对象
const themeStyle = {
    fillStyle: '#2c3e50',
    strokeStyle: '#ecf0f1',
    lineWidth: 2
};

<g-rect fill {...themeStyle} />
<g-circle stroke {...themeStyle} />