mirror of
				https://github.com/unanmed/HumanBreak.git
				synced 2025-11-04 15:12:58 +08:00 
			
		
		
		
	docs: UI 指南文档
This commit is contained in:
		
							parent
							
								
									e282d3f111
								
							
						
					
					
						commit
						5e1ea25d6d
					
				@ -5,12 +5,17 @@ export default defineConfig({
 | 
			
		||||
    title: 'HTML5 魔塔样板 V2.B',
 | 
			
		||||
    description: 'HTML5 魔塔样板 V2.B 帮助文档',
 | 
			
		||||
    base: '/_docs/',
 | 
			
		||||
    markdown: {
 | 
			
		||||
        math: true
 | 
			
		||||
    },
 | 
			
		||||
    themeConfig: {
 | 
			
		||||
        // https://vitepress.dev/reference/default-theme-config
 | 
			
		||||
        outline: [2, 3],
 | 
			
		||||
        nav: [
 | 
			
		||||
            { text: '主页', link: '/' },
 | 
			
		||||
            { text: '指南', link: '/guide/diff' },
 | 
			
		||||
            { text: 'API', link: '/api/' }
 | 
			
		||||
            { text: 'API', link: '/api/' },
 | 
			
		||||
            { text: '错误代码', link: '/logger/' }
 | 
			
		||||
        ],
 | 
			
		||||
        sidebar: {
 | 
			
		||||
            '/guide/': [
 | 
			
		||||
@ -18,7 +23,12 @@ export default defineConfig({
 | 
			
		||||
                    text: '深度指南',
 | 
			
		||||
                    items: [
 | 
			
		||||
                        { text: '差异说明', link: '/guide/diff' },
 | 
			
		||||
                        { text: '系统说明', link: '/guide/system' }
 | 
			
		||||
                        { text: '系统说明', link: '/guide/system' },
 | 
			
		||||
                        { text: 'UI 编写', link: '/guide/ui' },
 | 
			
		||||
                        { text: 'UI 优化', link: '/guide/ui-perf' },
 | 
			
		||||
                        { text: 'UI 系统', link: '/guide/ui-system' },
 | 
			
		||||
                        { text: 'UI 元素', link: '/guide/ui-elements' },
 | 
			
		||||
                        { text: 'UI 常见问题', link: '/guide/ui-faq' }
 | 
			
		||||
                    ]
 | 
			
		||||
                }
 | 
			
		||||
            ]
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,7 @@ lang: zh-CN
 | 
			
		||||
 | 
			
		||||
本文档暂时只会对新样板新增内容进行说明,其余请查看[旧样板文档](https://h5mota.com/games/template/_docs/#/)。
 | 
			
		||||
 | 
			
		||||
本指南建立在你已经大致了解 js 的基础语法的基础上。如果还不了解可以尝试对指南内容进行模仿,或者查看[人类塔解析](https://h5mota.com/bbs/thread/?tid=1018&p=1)
 | 
			
		||||
本指南建立在你已经大致了解 js 的基础语法的基础上。如果还不了解 js 语法可以尝试对指南内容进行模仿,或者查看[人类塔解析](https://h5mota.com/bbs/thread/?tid=1018&p=1)
 | 
			
		||||
 | 
			
		||||
如果你有能力直接使用源码版样板进行创作,也可以直接 fork 或 clone 2.B 样板[存储库](https://github.com/unanmed/HumanBreak/tree/template-v2.B)。2.B 样板使用了 vite 作为了构建工具,同时使用了 ts 等作为了开发语言。
 | 
			
		||||
 | 
			
		||||
@ -22,7 +22,7 @@ lang: zh-CN
 | 
			
		||||
-   使用全新的 UI 编写方式,速度快,效率高
 | 
			
		||||
-   模块化,可以使用 ES6 模块化语法
 | 
			
		||||
-   移除插件系统,可以自定义代码目录结构,更加自由
 | 
			
		||||
-   优化渲染端(client 端)与数据端(data 端)的通讯,渲染段现在可以直接引用数据端,不过数据端还不能直接引用渲染端
 | 
			
		||||
-   优化渲染端(client 端)与数据端(data 端)的通讯,渲染端现在可以直接引用数据端,不过数据端还不能直接引用渲染端
 | 
			
		||||
 | 
			
		||||
## 差异内容
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								docs/guide/img/image.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								docs/guide/img/image.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 22 KiB  | 
							
								
								
									
										3
									
								
								docs/guide/img/mermaid-diagram-2025-03-12-210212.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								docs/guide/img/mermaid-diagram-2025-03-12-210212.svg
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| 
		 After Width: | Height: | Size: 35 KiB  | 
@ -26,7 +26,7 @@ lang: zh-CN
 | 
			
		||||
-   [@motajs/system](../api/motajs-system)
 | 
			
		||||
-   [@motajs/system-action](../api/motajs-system-action)
 | 
			
		||||
-   [@motajs/system-ui](../api/motajs-system-ui)
 | 
			
		||||
-   [@motajs/types](../api/types)
 | 
			
		||||
-   [@motajs/types](../api/motajs-types)
 | 
			
		||||
-   [@user/client-modules](../api/user-client-modules)
 | 
			
		||||
-   [@user/data-base](../api/user-data-base)
 | 
			
		||||
-   [@user/data-fallback](../api/user-data-fallback)
 | 
			
		||||
@ -97,13 +97,11 @@ hook.on('afterBattle', enemy => {
 | 
			
		||||
 | 
			
		||||
    1. 加载渲染端入口
 | 
			
		||||
    2. 加载数据端入口
 | 
			
		||||
    3. 并行初始化数据端,写入 `Mota` 全局变量
 | 
			
		||||
    4. 初始化完毕后执行 `loading.emit('dataRegistered')` 钩子
 | 
			
		||||
    5. 并行初始化渲染端
 | 
			
		||||
    6. 初始化完毕后执行 `loading.emit('clientRegistered')` 钩子
 | 
			
		||||
    7. 二者都初始化完毕后执行 `loading.emit('registered')` 钩子
 | 
			
		||||
    8. 执行数据端各个模块的初始化函数
 | 
			
		||||
    9. 执行渲染段各个模块的初始化函数
 | 
			
		||||
    3. 并行初始化数据端与渲染端,在数据端写入 `Mota` 全局变量
 | 
			
		||||
    4. 数据端初始化完毕后执行 `loading.emit('dataRegistered')` 钩子,渲染端初始化完毕后执行 `loading.emit('clientRegistered')` 钩子
 | 
			
		||||
    5. 二者都初始化完毕后执行 `loading.emit('registered')` 钩子
 | 
			
		||||
    6. 执行数据端各个模块的初始化函数
 | 
			
		||||
    7. 执行渲染端各个模块的初始化函数
 | 
			
		||||
 | 
			
		||||
4. 如果是录像验证中:
 | 
			
		||||
 | 
			
		||||
@ -121,6 +119,10 @@ hook.on('afterBattle', enemy => {
 | 
			
		||||
11. 资源加载完毕后执行 `loading.emit('loaded')` 钩子
 | 
			
		||||
12. 进入标题界面
 | 
			
		||||
 | 
			
		||||
使用流程图表示如下:
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
## 函数重写
 | 
			
		||||
 | 
			
		||||
在 2.B 模式下,如果想改 `libs` 的内容,如果直接在里面改会很麻烦,而且两端通讯也不方便,因此我们建议在 `package-user` 中对函数重写,这样的话就可以使用模块化语法,更加方便。同时,2.B 也提供了函数重写接口,他在 `@motajs/legacy-common` 模块中,我们可以这么使用它:
 | 
			
		||||
@ -146,7 +148,7 @@ export function patchMyFunctions() {
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
然后,我们找到 `client-modules` 文件夹下的 `index.ts` 文件,然后在 `create` 函数中调用 `patchMyFunctions`,这样我们的函数重写就完成了。
 | 
			
		||||
然后,我们找到 `client-modules` 文件夹下的 `index.ts` 文件,然后在 `create` 函数中引入并调用 `patchMyFunctions`,这样我们的函数重写就完成了。**注意**,如果两个重写冲突,会在控制台弹出警告,并使用最后一次重写的内容。
 | 
			
		||||
 | 
			
		||||
::: warning
 | 
			
		||||
**注意**,在渲染端重写的函数在录像验证中将无效,因为录像验证不会执行任何渲染端内容!
 | 
			
		||||
@ -155,3 +157,32 @@ export function patchMyFunctions() {
 | 
			
		||||
## 目录结构
 | 
			
		||||
 | 
			
		||||
我们建议每个文件夹中都有一个 `index.ts` 文件,将本文件夹中的其他文件经由此文件导出,这样方便管理,同时结构清晰。可以参考 `packages-user/client-modules` 文件夹中是如何做的。
 | 
			
		||||
 | 
			
		||||
## ES6 模块化语法
 | 
			
		||||
 | 
			
		||||
我们推荐使用 ES6 模块化语法来编写代码,这会大大提高开发效率。下面来简单说明一下模块化语法的用法,首先是引入其他模块:
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
import { Patch } from '@motajs/legacy-common'; // 从样板库中引入接口
 | 
			
		||||
// 引入本地文件,注意不要填写后缀名,只可以在同一个 packages-user 子文件夹下使用
 | 
			
		||||
// 不可以跨文件夹使用,例如 packages-user/client-modules 就不能直接引用 packages-user/data-base 文件夹
 | 
			
		||||
// 需要使用 import { ... } from '@user/data-base'
 | 
			
		||||
import { patchMyFunctions } from './override';
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
然后是从当前模块导出内容:
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
// 导出函数
 | 
			
		||||
export function myFunc() { ... }
 | 
			
		||||
// 导出变量/常量
 | 
			
		||||
export const num = 100;
 | 
			
		||||
// 导出类
 | 
			
		||||
export class MyClass { ... }
 | 
			
		||||
// 从另一个模块中导出全部内容,即将另一个模块的内容转发为当前模块
 | 
			
		||||
export * from './xxx';
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
更多模块化语法内容请查看[这个文档](https://h5mota.com/bbs/thread/?tid=1018&p=3#p33)
 | 
			
		||||
 | 
			
		||||
与 TypeScript 相关语法请查看[这个文档](https://h5mota.com/bbs/thread/?tid=1018&p=3#p41)
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										754
									
								
								docs/guide/ui-elements.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										754
									
								
								docs/guide/ui-elements.md
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,754 @@
 | 
			
		||||
# UI 元素
 | 
			
		||||
 | 
			
		||||
本节将会讲解 UI 系统中常用的渲染元素以及基础使用。
 | 
			
		||||
 | 
			
		||||
## 通用属性
 | 
			
		||||
 | 
			
		||||
UI 元素包含很多通用属性,我们先来介绍这些属性,它们可以用在任何渲染元素和 UI 组件中。
 | 
			
		||||
 | 
			
		||||
### 定位属性
 | 
			
		||||
 | 
			
		||||
元素包含若干定位属性,其中最常用的是 `loc` 属性,我们也推荐全部使用这个属性来修改元素定位。其类型声明如下:
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
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]` 表示锚点横坐标在元素左侧一个元素宽度的位置,纵坐标在元素下边缘的位置。
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
示例如下:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
// 元素相对于 32, 32 位置居中(锚点在元素正中间),宽高为 64
 | 
			
		||||
<sprite loc={[32, 32, 64, 64, 0.5, 0.5]} />
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
除了 `loc` 属性之外,还可以通过设置 `anc` 属性来修改锚点位置,示例如下:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
// 设置锚点,效果为靠右对齐,上下居中对齐
 | 
			
		||||
<sprite anc={[1, 0.5]} />
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
你还可以手动指定 `x` `y` `width` `height` `anchorX` `anchorY` 属性,但是这种方式比较啰嗦,并不建议使用:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
<sprite x={32} y={32} width={64} height={64} anchorX={0.5} anchorY={0.5} />
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
最后说明一下元素的 `type` 属性,此属性描述了元素的定位模式,默认为 `static` 常规定位,此定位模式下元素位置会按照上述内容更改,而在 `absolute` 模式下,不论怎么修改定位属性,它都会保持在左上角的位置,可能会在一些特殊场景下使用(极度不建议使用此属性,很可能在 2.B.1 版本就会将其删除)。
 | 
			
		||||
 | 
			
		||||
### 纵深属性
 | 
			
		||||
 | 
			
		||||
可以通过 `zIndex` 属性来调整一个元素的纵深。纵深描述了元素之间的重叠关系,纵深高的会处在纵深低的元素上方,同时也会阻碍交互事件向纵深低的元素传播。必要的时候,需要通过设置纵深属性来调整层级关系。未设置时,后面的元素会处在前面的元素之上。
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
// 这个元素会处在上层
 | 
			
		||||
<sprite zIndex={10} />
 | 
			
		||||
// 这个元素会处在下层
 | 
			
		||||
<sprite zIndex={5} />
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### 效果属性
 | 
			
		||||
 | 
			
		||||
效果属性包含 `filter` `composite` 及 `alpha` 三个属性。
 | 
			
		||||
 | 
			
		||||
`filter` 表示此元素的滤镜,参考 [CanvasRenderingContext2D.filter](https://developer.mozilla.org/zh-CN/docs/Web/CSS/filter),可以填写内置函数或 svg 滤镜。默认不包含任何滤镜。示例如下:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
// 亮度变为 150%,对比度变为 120%
 | 
			
		||||
<sprite filter="brightness(150%) contrast(120%)" />
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
`composite` 属性描述了当前元素与在此之前渲染的元素之间的混合模式,参考 [CanvasRenderingContext2D.globalCompositeOperation](https://developer.mozilla.org/zh-CN/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation),可以填写 26 个值。默认使用简单 `alpha` 混合,即 `source-over`。例如:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
// 使用加算方式叠加,两个颜色的 RGB 值分别相加得到最终结果
 | 
			
		||||
<sprite composite="lighter" />
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
`alpha` 属性描述了此元素的不透明度,1 表示完全不透明,0 表示完全透明。在叠加时,所有颜色都会乘以此不透明度后叠加。默认值是完全不透明,即 1。但需要注意的是,虽然 1 表示完全不透明,但是如果画布内容本身包含透明内容(例如一个半透明矩形),即使是 1 也会表现为透明,因为叠加时会乘以 1,不透明度不变。示例如下:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
// 一个半透明元素
 | 
			
		||||
<sprite alpha={0.5} />
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### 缓存属性
 | 
			
		||||
 | 
			
		||||
可以通过 `cache` 和 `nocache` 属性来指定这个元素的缓存行为,其中 `nocache` 表示禁用此元素的缓存机制,优先级最高,设置后必然不使用缓存。`cache` 表示启用此元素的缓存行为,常用于一些默认不启用缓存的元素,优先级低于 `cache`。这两个元素都不能动态设置,也就是说不能使用响应式来修改其值。示例如下:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
// 内部渲染内容比较简单,不需要启用缓存
 | 
			
		||||
<container nocache>
 | 
			
		||||
    <text />
 | 
			
		||||
</container>
 | 
			
		||||
// 路径较为复杂,因此启用 g-path 的缓存行为
 | 
			
		||||
<g-path path={veryComplexPath} cache />
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### 元素溢出行为
 | 
			
		||||
 | 
			
		||||
溢出行为是指,当其子元素超出父元素的大小时,执行的行为。例如,假如父元素大小为 `200 * 200`,里面有一个子元素,大小为 `100 * 100`,位于 `(150, 50)` 的位置,这时候子元素的一部分就会超出父元素的范围。
 | 
			
		||||
 | 
			
		||||
在本渲染系统中,所有元素的默认溢出行为是裁剪,即不会显示任何溢出内容,注意调整容器的宽高。在 `nocache` 模式下,由于不受到缓存的约束,溢出内容依然会显示,不过不建议利用此特性来编写 UI,因为这种行为可能会在后续的更新中修改。
 | 
			
		||||
 | 
			
		||||
### 隐藏元素
 | 
			
		||||
 | 
			
		||||
可以使用 `hidden` 属性来隐藏元素:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
const hidden = ref(false);
 | 
			
		||||
// 一般使用一个响应式变量来控制隐藏行为,因为设置成常量没有任何意义
 | 
			
		||||
<sprite hidden={hidden.value} />;
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### 交互属性
 | 
			
		||||
 | 
			
		||||
交互属性包括 `cursor` 和 `noevent`。前者描述了鼠标覆盖在当前元素上时的指针样式,参考 [CSS: cursor](https://developer.mozilla.org/zh-CN/docs/Web/CSS/cursor)。示例如下:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
// 鼠标放置在该元素上时使用小手样式
 | 
			
		||||
<sprite cursor="pointer" />
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
`noevent` 表明当前元素将不会触发任何事件,事件将会下穿至纵深更低的元素。示例如下:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
// 设置为 noevent 模式
 | 
			
		||||
<sprite zIndex={100} noevent />
 | 
			
		||||
// 这样的话这个 onClick 就可以正常触发了
 | 
			
		||||
<sprite zIndex={10} onClick={click} />
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### 高清与抗锯齿
 | 
			
		||||
 | 
			
		||||
包含 `hd` `anti` `noanti` 三个属性,`hd` 表示是否启用高清,大部分元素是默认启用的,除了几个像素风为主的元素(地图渲染等);`anti` 表示手动启用画布的抗锯齿行为,一般用于默认不启用抗锯齿的元素;`noanti` 表示手动关闭元素的抗锯齿行为,优先级高于 `anti`,一般用于像素风图片展示、图标显示等,同时也有助于提高渲染性能。
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
// 关闭高清
 | 
			
		||||
<sprite hd={false} />
 | 
			
		||||
// 关闭抗锯齿
 | 
			
		||||
<sprite noanti />
 | 
			
		||||
// 启用地图渲染的抗锯齿
 | 
			
		||||
<layer anti />
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### 元素变换属性
 | 
			
		||||
 | 
			
		||||
可以通过调整 `transform` 属性来修改元素的线性变换,包括平移、旋转、缩放。如果是简易的变换,可以使用 `rotate` `scale` 属性来修改旋转、缩放,使用 `loc` 来修改位置:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
// 旋转 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 类核心功能
 | 
			
		||||
 | 
			
		||||
首先创建其实例:
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
import { Transform } from '@motajs/render';
 | 
			
		||||
 | 
			
		||||
const trans = new Transform();
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
之后可以链式调用来修改矩阵:
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
// 链式调用示例
 | 
			
		||||
trans
 | 
			
		||||
    .setTranslate(100, 50)
 | 
			
		||||
    .rotate(Math.PI / 4)
 | 
			
		||||
    .scale(2, 1.5);
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
方法对比
 | 
			
		||||
 | 
			
		||||
| 方法类型 | 特点                       | 函数                                                 |
 | 
			
		||||
| -------- | -------------------------- | ---------------------------------------------------- |
 | 
			
		||||
| 叠加变换 | 在现有变换基础上叠加新变换 | `translate` `rotate` `scale` `transform`             |
 | 
			
		||||
| 直接设置 | 覆盖当前变换参数           | `setTranslate` `setRotate` `setScale` `setTransform` |
 | 
			
		||||
 | 
			
		||||
#### 关键方法详解
 | 
			
		||||
 | 
			
		||||
平移变换:
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
// 相对移动(叠加)
 | 
			
		||||
trans.translate(20, -10);
 | 
			
		||||
// 绝对定位(覆盖)
 | 
			
		||||
trans.setTranslate(200, 150);
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
旋转变换:
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
// 叠加旋转 45 度
 | 
			
		||||
trans.rotate(Math.PI / 4);
 | 
			
		||||
// 设置绝对旋转角度
 | 
			
		||||
trans.setRotate(Math.PI / 2);
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
缩放变换:
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
// X 轴放大 2 倍,Y 轴不变(叠加)
 | 
			
		||||
trans.scale(2, 1);
 | 
			
		||||
// 设置绝对缩放比例
 | 
			
		||||
trans.setScale(0.5, 0.8);
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
#### 高级功能
 | 
			
		||||
 | 
			
		||||
矩阵操作:
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
// 手动设置变换矩阵
 | 
			
		||||
trans.setTransform(
 | 
			
		||||
    1,
 | 
			
		||||
    0, // 缩放部分
 | 
			
		||||
    0,
 | 
			
		||||
    1, // 旋转部分
 | 
			
		||||
    100,
 | 
			
		||||
    50 // 平移部分
 | 
			
		||||
);
 | 
			
		||||
// 矩阵相乘(组合变换)
 | 
			
		||||
const combined = trans.multiply(otherTransform);
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
坐标变换:
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
// 将局部坐标转换为世界坐标,即计算一个坐标经过此变换矩阵计算后的位置
 | 
			
		||||
const worldPos = trans.transformed(10, 20);
 | 
			
		||||
// 将世界坐标转换回局部坐标,即计算一个坐标经过此变换矩阵逆转换后的位置
 | 
			
		||||
const localPos = trans.untransformed(150, 80);
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
#### 性能优化技巧
 | 
			
		||||
 | 
			
		||||
使用自动更新机制:
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
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)
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
// 正确顺序示例
 | 
			
		||||
trans
 | 
			
		||||
    .setScale(2)
 | 
			
		||||
    .rotate(Math.PI / 3)
 | 
			
		||||
    .setTranslate(100, 50);
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
组合复杂变换:
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
// 创建子变换
 | 
			
		||||
const childTrans = trans
 | 
			
		||||
    .clone() // 从 trans 复制一个相同的变换出来,以防止修改原变换
 | 
			
		||||
    .rotate(-Math.PI / 6)
 | 
			
		||||
    .translate(30, 0);
 | 
			
		||||
 | 
			
		||||
// 应用组合变换
 | 
			
		||||
const finalTrans = trans.multiply(childTrans);
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
#### 应用到元素
 | 
			
		||||
 | 
			
		||||
赋值给 `transform` 属性即可
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
<sprite transform={finalTrans} />
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
#### 常见问题排查
 | 
			
		||||
 | 
			
		||||
1. 变换不生效?
 | 
			
		||||
 | 
			
		||||
    - 验证绑定的对象是否实现 `updateTransform`
 | 
			
		||||
    - 检查有没有把 `trans` 对象赋值给元素的 `transform` 属性
 | 
			
		||||
 | 
			
		||||
2. 性能问题
 | 
			
		||||
 | 
			
		||||
    - 避免高频调用 `setTransform`
 | 
			
		||||
    - 优先使用叠加方法代替矩阵直接操作
 | 
			
		||||
    - 利用 `clone()` 复用已有变换
 | 
			
		||||
 | 
			
		||||
## `sprite` 标签
 | 
			
		||||
 | 
			
		||||
`sprite` 标签是一个允许你自定义渲染内容的标签,它新增了一个属性 `render` 属性,允许你传入一个函数来执行自定义渲染。函数定义如下:
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
type RenderFn = (canvas: MotaOffscreenCanvas2D, transform: Transform) => void;
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
-   `canvas`: 要渲染至的画布,一般直接将内容渲染至这个画布上
 | 
			
		||||
-   `transform`: 当前元素的变换矩阵,相对于父元素,不常用
 | 
			
		||||
 | 
			
		||||
多数情况下,我们只会使用到第一个参数,`MotaOffscreenCanvas2D` 接口请参考 [API 文档](../api/motajs-render-core)。下面是一个典型案例:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
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` 参数,定义如下:
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
type CustomContainerRenderFn = (
 | 
			
		||||
    canvas: MotaOffscreenCanvas2D,
 | 
			
		||||
    children: RenderItem[],
 | 
			
		||||
    transform: Transform
 | 
			
		||||
) => void;
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
-   `canvas`: 要渲染至的画布,一般直接将内容渲染至这个画布上
 | 
			
		||||
-   `children`: 要渲染的子元素,按 `zIndex` 升序排列
 | 
			
		||||
-   `transform`: 当前元素的变换矩阵,相对于父元素,不常用
 | 
			
		||||
 | 
			
		||||
典型案例如下:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
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` 标签用于显示文字,它会自动计算文字的宽高并设置为元素宽高,因此不要手动指定宽高,否则可能会引起位置错误。它新增了这些属性:
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
interface TextProps extends BaseProps {
 | 
			
		||||
    /** 要渲染的文字 */
 | 
			
		||||
    text?: string;
 | 
			
		||||
    /** 文字的填充样式 */
 | 
			
		||||
    fillStyle?: CanvasStyle;
 | 
			
		||||
    /** 文字的描边样式 */
 | 
			
		||||
    strokeStyle?: CanvasStyle;
 | 
			
		||||
    /** 文字的字体 */
 | 
			
		||||
    font?: Font;
 | 
			
		||||
    /** 文字的描边粗细 */
 | 
			
		||||
    strokeWidth?: number;
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
典型案例如下:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
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` 属性,传入图片对象(注意不是注册图片名称)。用例如下:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
// 获取注册的图片
 | 
			
		||||
const img = core.material.images.images['myImage.png'];
 | 
			
		||||
// 显示图片
 | 
			
		||||
<image image={img} />;
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## `icon` 标签
 | 
			
		||||
 | 
			
		||||
`icon` 标签用于显示一个图标,可以包含动画帧。它有如下参数:
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
export interface IconProps extends BaseProps {
 | 
			
		||||
    /** 图标 id 或数字 */
 | 
			
		||||
    icon: AllNumbers | AllIds;
 | 
			
		||||
    /** 显示图标的第几帧 */
 | 
			
		||||
    frame?: number;
 | 
			
		||||
    /** 是否开启动画,开启后 frame 参数无效 */
 | 
			
		||||
    animate?: boolean;
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
使用案例如下:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
// 显示绿史莱姆,开启动画
 | 
			
		||||
<icon icon="greenSlime" animate />
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## `winskin` 标签
 | 
			
		||||
 | 
			
		||||
`winskin` 标签允许你显示一个 rpg maker 风格的背景图(window skin),它有如下参数:
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
export interface WinskinProps extends BaseProps {
 | 
			
		||||
    /** winskin 的图片 id */
 | 
			
		||||
    image: ImageIds;
 | 
			
		||||
    /** 边框大小 */
 | 
			
		||||
    borderSize?: number;
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
其中边框大小默认为 32,表示上边框和下边框加起来共 32 像素,即四周边框 16 像素宽。用例如下:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
// 使用 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` 即可,示例如下:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
// 基础矩形,矩形默认仅填充模式,因此如果需要描边的话需要手动指定 fill 和 stroke 参数
 | 
			
		||||
// 注意如果仅指定 stroke 参数的话,会变为仅描边形式
 | 
			
		||||
<g-rect loc={[100, 100, 200, 150]} fill stroke fillStyle="#f0f" lineWidth={2} />
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### 圆形和扇形 `<g-circle>`
 | 
			
		||||
 | 
			
		||||
参数如下:
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
interface CirclesProps {
 | 
			
		||||
    radius: number; // 半径
 | 
			
		||||
    start?: number; // 起始弧度(默认0)
 | 
			
		||||
    end?: number; // 结束弧度(默认2π)
 | 
			
		||||
    /**
 | 
			
		||||
     * 圆属性参数,可以填 `[圆心 x 坐标,圆心 y 坐标,半径,起始角度,终止角度]`,是 x, y, radius, start, end 的简写,
 | 
			
		||||
     * 其中半径可选,后两项要么都填,要么都不填
 | 
			
		||||
     */
 | 
			
		||||
    circle?: CircleParams;
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
示例如下:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
// 完整圆形
 | 
			
		||||
<g-circle circle={[300, 200, 10]} fillStyle="skyblue" />
 | 
			
		||||
// 扇形(60度到180度)
 | 
			
		||||
<g-circle  circle={[400, 300, 40, Math.PI/3, Math.PI]} />
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### 直线 `<g-line>`
 | 
			
		||||
 | 
			
		||||
核心参数:
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
interface LineProps {
 | 
			
		||||
    x1: number; // 起点X
 | 
			
		||||
    y1: number; // 起点Y
 | 
			
		||||
    x2: number; // 终点X
 | 
			
		||||
    y2: number; // 终点Y
 | 
			
		||||
    /** 直线属性简写参数,可以填 `[x1, y1, x2, y2]`,都是必填 */
 | 
			
		||||
    line: [number, number, number, number];
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
示例如下:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
// 普通直线
 | 
			
		||||
<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>`
 | 
			
		||||
 | 
			
		||||
核心参数:
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
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;
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
示例如下:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
// 三次贝塞尔曲线
 | 
			
		||||
<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>`
 | 
			
		||||
 | 
			
		||||
核心参数:
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
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;
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
示例如下:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
// 二次贝塞尔曲线
 | 
			
		||||
<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 类似,如下:
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
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;
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
示例如下:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
// 四角圆角半径都为 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>`
 | 
			
		||||
 | 
			
		||||
核心参数:
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
interface PathProps {
 | 
			
		||||
    path?: Path2D; // 自定义路径对象
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
示例:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
// 创建五角星
 | 
			
		||||
const starPath = new Path2D();
 | 
			
		||||
// ...路径绘制逻辑
 | 
			
		||||
<g-path
 | 
			
		||||
    path={starPath}
 | 
			
		||||
    fill
 | 
			
		||||
    stroke
 | 
			
		||||
    fillStyle="orange"
 | 
			
		||||
    strokeStyle="#c00"
 | 
			
		||||
    lineWidth={2}
 | 
			
		||||
/>;
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### 最佳实践建议
 | 
			
		||||
 | 
			
		||||
1. 交互增强:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
<g-rect
 | 
			
		||||
    fill
 | 
			
		||||
    stroke
 | 
			
		||||
    actionStroke // 仅在描边区域响应点击
 | 
			
		||||
    onClick={handleSelect}
 | 
			
		||||
/>
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
2. 样式复用:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
// 创建样式对象
 | 
			
		||||
const themeStyle = {
 | 
			
		||||
    fillStyle: '#2c3e50',
 | 
			
		||||
    strokeStyle: '#ecf0f1',
 | 
			
		||||
    lineWidth: 2
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
<g-rect fill {...themeStyle} />
 | 
			
		||||
<g-circle stroke {...themeStyle} />
 | 
			
		||||
```
 | 
			
		||||
							
								
								
									
										61
									
								
								docs/guide/ui-faq.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								docs/guide/ui-faq.md
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,61 @@
 | 
			
		||||
# UI 常见问题
 | 
			
		||||
 | 
			
		||||
## 为什么我的 UI 不显示?
 | 
			
		||||
 | 
			
		||||
检查 UI 的纵深(zIndex)是否符合预期,有没有被其他元素遮挡;检查当前元素是否处于 `hidden` 状态。可以在控制台输入 `logTagTree()` 来输出当前的渲染树 `xml` 标签结构,会包含一些重要信息。
 | 
			
		||||
 | 
			
		||||
第二种可能性是你的元素处在了父元素范围之外,导致被裁剪掉。注意,`transform` 属性是对元素本身的变换,这也会导致元素本身的矩形范围发生变化,如果你的元素设置了缩放、旋转等,需要考虑此属性对位置的影响。
 | 
			
		||||
 | 
			
		||||
## 为什么我的元素 onClick 事件没办法触发?
 | 
			
		||||
 | 
			
		||||
可能你的元素被纵深更高的元素覆盖,导致事件无法传播至你的元素,考虑在纵深更高的元素上添加 `noevent` 标识来禁用它的事件传播。注意,一个纯透明的元素也可能会覆盖你的元素,仔细查看你的渲染树结构。
 | 
			
		||||
 | 
			
		||||
第二种可能性是其子元素拦截了冒泡或是其父元素拦截了捕获,检查 `e.stopPropagation` 调用情况。
 | 
			
		||||
 | 
			
		||||
## 我的数据更新后,为什么渲染内容没有更新?
 | 
			
		||||
 | 
			
		||||
你可能在使用 `sprite` 元素,然后在渲染函数里面调用了外部数据,这样的话当外部数据更新时,你的 `sprite` 元素并不会自动更新,需要手动更新。手动更新参考代码:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
import { Sprite } from '@motajs/render';
 | 
			
		||||
 | 
			
		||||
const mySprite = ref<Sprite>();
 | 
			
		||||
// 数据更新时,同时更新 sprite 元素
 | 
			
		||||
watch(data, () => mySprite.value?.update());
 | 
			
		||||
 | 
			
		||||
// 将 mySprite 传入 ref 参数,这样当挂载完毕后就会将 mySprite.value 设置为该元素
 | 
			
		||||
<sprite ref={mySprite} render={render} />;
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
除此之外还可能你的数据不是响应式数据,确保你的数据经过了 `reactive` 或 `ref` 包裹。
 | 
			
		||||
 | 
			
		||||
## 我的 UI 很卡
 | 
			
		||||
 | 
			
		||||
可能使用了平铺式布局,建议使用 `Scroll` 组件或者 `Page` 组件来对平铺内容分割,从而提高渲染效率。可以参考对应的 [API 文档](../api/user-client-modules/Scroll)。
 | 
			
		||||
 | 
			
		||||
## 玩着玩着突然黑屏了一下,然后画面就不显示了
 | 
			
		||||
 | 
			
		||||
你应该遇到了内存泄漏问题,当一个元素被卸载后,它应该会被销毁,但是如果没有被预期销毁,那么会导致内存泄漏,最终导致爆显存,就会导致画面黑屏一下,然后内容就会不显示。样板本身已经针对这个问题进行了处理,一般情况下不会出现问题,出现这个问题时大概率是你自己的组件或 UI 有问题。可能原因有很多,例如你声明了一个列表,当组件挂载时将元素放入列表,但是当组件卸载时,你却没有将元素移除,这时候就会导致这个元素无法正确被垃圾回收,从而引起内存泄漏。
 | 
			
		||||
 | 
			
		||||
关于这个问题的最佳实践:
 | 
			
		||||
 | 
			
		||||
-   如果你手动存储了一些元素,确保在卸载时将它们删除
 | 
			
		||||
-   在删除它们的同时,调用它们的 `destroy` 方法,来确保可以被垃圾回收
 | 
			
		||||
-   在控制台输入 `Mota.require('@motajs/render').MotaOffscreenCanvas2D.list` 来查看当前还有哪些画布正在使用,游玩一段时间后再次输入,检查数量是否增长,如果增长,说明发生了内存泄漏
 | 
			
		||||
-   确保组件卸载时已经清空了定时器等内容
 | 
			
		||||
-   如果需要每帧执行函数,请使用 `onTick` 接口,而非其他方法
 | 
			
		||||
 | 
			
		||||
如果你直接使用 `MotaOffscreenCanvas2D` 接口,请确保:
 | 
			
		||||
 | 
			
		||||
-   在使用前调用了 `activate` 方法
 | 
			
		||||
-   在使用后调用了 `deactivate` 方法
 | 
			
		||||
-   如果不需要再修改画布属性,只需要绘制,请调用 `freeze` 方法
 | 
			
		||||
-   如果之后不再使用该画布,请调用 `destroy` 方法
 | 
			
		||||
 | 
			
		||||
## 为什么我的滤镜不显示?
 | 
			
		||||
 | 
			
		||||
很遗憾,截止目前(2.B 发布日期),IOS 依然没有支持 `CanvasRenderingContext2D` 上的 `filter` 方法,所有滤镜属性在 IOS 上将不会显示。不过,我们提供了 `Shader` 元素,它使用 `WebGL2` 接口,允许你制作自己的滤镜,如果滤镜是必要的,请考虑使用此元素,但是需要一定的图形学基础,可以在造塔群询问我或造塔辅助 AI。
 | 
			
		||||
 | 
			
		||||
## 不同设备的显示内容会不一样吗?
 | 
			
		||||
 | 
			
		||||
从理论上来讲,除了上面那个问题提到的滤镜,其他的所有内容的渲染结果应该完全一致,如果出现了不一致的情况,请上报样板 bug。
 | 
			
		||||
							
								
								
									
										142
									
								
								docs/guide/ui-perf.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								docs/guide/ui-perf.md
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,142 @@
 | 
			
		||||
---
 | 
			
		||||
lang: zh-CN
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
# UI 优化指南
 | 
			
		||||
 | 
			
		||||
多数情况下,我们编写的简单 UI 并不需要特别的性能优化,渲染系统的懒更新及缓存机制已经可以有很优秀的性能表现,不过我们还是可能会遇到一些需要特殊优化的场景,本节将会讲述如何优化 UI 的性能表现,优化建议包括:避免元素平铺;使用 `Scroll` 或 `Page` 组件优化平铺性能;避免元素自我更新;使用 `cache` 和 `nocache` 标识;特殊场景禁用抗锯齿和高清;在合适场景下隐藏一些元素等。
 | 
			
		||||
 | 
			
		||||
## 避免元素平铺
 | 
			
		||||
 | 
			
		||||
在不使用 `Scroll` 组件时,我们需要尽量避免元素平铺,因为这会导致更新时渲染次数上升,从而引起性能下降。我们建议善用树形结构的缓存特性,将可以作为一个整体的元素使用一个容器 `container` 包裹起来,减少更新时的渲染次数,尤其是对于那些不常更新的元素来说,更应该使用容器包裹。不过我们也不建议嵌套过深,这可能导致浪费在递归渲染上的时间过长,渲染效率变低。
 | 
			
		||||
 | 
			
		||||
画布渲染树的深度遍历特性使得:
 | 
			
		||||
 | 
			
		||||
-   每个独立容器的更新会触发子树的重新渲染
 | 
			
		||||
-   容器层级过深会增加递归调用栈开销
 | 
			
		||||
-   合理分组可将高频/低频更新元素隔离
 | 
			
		||||
 | 
			
		||||
下面是代码示例:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
// ❌ 差的写法,全部平铺在一个容器里
 | 
			
		||||
<container>
 | 
			
		||||
    <text text="1" />
 | 
			
		||||
    {/* 中间省略 998 个元素 */}
 | 
			
		||||
    <text text="1000" />
 | 
			
		||||
</container>
 | 
			
		||||
 | 
			
		||||
// ✅ 好的写法
 | 
			
		||||
<container>
 | 
			
		||||
    {/* 把不常更新的单独放到一个容器里面 */}
 | 
			
		||||
    <container>
 | 
			
		||||
        <text text="1" />
 | 
			
		||||
        {/* 中间省略 988 个元素 */}
 | 
			
		||||
        <text text="990" />
 | 
			
		||||
    </container>
 | 
			
		||||
    {/* 把常更新的单独放到一个容器里面 */}
 | 
			
		||||
    <container>
 | 
			
		||||
        <text text="991" />
 | 
			
		||||
        {/* 中间省略 8 个元素 */}
 | 
			
		||||
        <text text="1000" />
 | 
			
		||||
    </container>
 | 
			
		||||
</container>
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## 使用 `Scroll` 或 `Page` 组件优化平铺性能
 | 
			
		||||
 | 
			
		||||
在一些特殊情况下,我们不得不使用平铺布局,例如上一节提到的怪物手册,或是展示一个列表等,这时候必须平铺元素。这时候我们可以使用 `Scroll` 组件或 `Page` 组件来优化性能表现。`Scroll` 组件中,只有在画面内的元素会被渲染,而画面外的不会被渲染,这会大大提高渲染效率;`Page` 组件允许你把列表拆分成多个部分,然后把内容放在不同页中,从而提高渲染性能。极端情况下,`Page` 组件的渲染效率要明显高于 `Scroll` 组件,但是滚动条对于交互更友好,我们推荐在简单场景下使用 `Scroll` 组件,而对于复杂场景,换为 `Page` 组件。两个组件的使用方式可以参考 [API 文档](../api/motajs-render-elements/)。
 | 
			
		||||
 | 
			
		||||
我们建议:
 | 
			
		||||
 | 
			
		||||
1. **优先使用 Scroll**:
 | 
			
		||||
    - 元素数量 < 500
 | 
			
		||||
    - 需要流畅滚动交互
 | 
			
		||||
    - 元素高度不固定
 | 
			
		||||
2. **切换至 Page**:
 | 
			
		||||
    - 元素数量 > 1000
 | 
			
		||||
    - 需要支持快速跳转
 | 
			
		||||
    - 存在复杂子组件(如嵌套动画)
 | 
			
		||||
 | 
			
		||||
下面是代码示例:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
// ❌ 差的写法,全部平铺在一个容器里
 | 
			
		||||
<container>
 | 
			
		||||
    <text text="1" />
 | 
			
		||||
    {/* 中间省略 998 个元素 */}
 | 
			
		||||
    <text text="1000" />
 | 
			
		||||
</container>
 | 
			
		||||
 | 
			
		||||
// ✅ 好的写法,使用 Scroll 组件优化
 | 
			
		||||
<Scroll>
 | 
			
		||||
    <text text="1" />
 | 
			
		||||
    {/* 中间省略 998 个元素 */}
 | 
			
		||||
    <text text="1000" />
 | 
			
		||||
</Scroll>
 | 
			
		||||
 | 
			
		||||
// ✅ 好的写法,使用 Page 组件优化
 | 
			
		||||
<Page>
 | 
			
		||||
    {(page: number) => {
 | 
			
		||||
        return list.slice(page * 10, (page + 1) * 10).map(v => <text text={v.toString()} />)
 | 
			
		||||
    }}
 | 
			
		||||
</Page>
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## 避免元素自我更新
 | 
			
		||||
 | 
			
		||||
元素自我更新是指,在元素的渲染函数内,触发了元素的冒泡更新,这会导致更新无限循环,而且难以察觉。为了解决难以察觉的问题,我们使用了一种方式来专门探测这种情况。常见的触发元素自我更新的场景就是使用 `sprite` 元素,例如:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
const element = ref<Sprite>();
 | 
			
		||||
const render = () => {
 | 
			
		||||
    element.value?.update();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
<sprite render={render} ref={element} />;
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
在上面这段渲染代码中,`sprite` 元素的渲染函数又再次触发了自我更新,这会导致更新无限循环。在开发环境下,这种情况会在控制台抛出警告:`Unexpected recursive call of Sprite.update?uid in render function. Please ensure you have to do this, if you do, ignore this warn.`,这会告诉你是哪个类型的元素触发了循环更新,以及对应元素的 `uid`,从而帮助你寻找问题所在。不过,样板还是留出了一个口子,如果你必须使用循环更新,那么你可以忽略此条警告,在网站上游玩时这条警告将不会被触发,游戏会正常运行。
 | 
			
		||||
 | 
			
		||||
## 使用 `cache` 和 `nocache` 标识
 | 
			
		||||
 | 
			
		||||
`cache` 和 `nocache` 表示可以让你更加精确地控制渲染树的缓存行为,从而更好地优化渲染性能。默认情况下,这些元素是会被缓存的:`container` `container-custom` `template` `sprite` `image` `icon` `layer` `layer-group` `animation`,对于这些元素,你可以使用 `nocache` 标识来禁用它们的缓存,对于其本身或其子元素的渲染较为简单的场景,禁用缓存后渲染效率可能会更高。其他元素默认是禁用缓存的,如果你的渲染内容比较复杂,例如 `g-path` 元素的路径很复杂,可以使用 `cache` 表示来启用缓存,从而提高渲染效率。示例代码如下:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
const render = (canvas: MotaOffscreenCanvas2D) => {
 | 
			
		||||
    canvas.ctx.fillRect(0, 0, 200, 200);
 | 
			
		||||
};
 | 
			
		||||
// ❌ 差的写法,一个简单的矩形绘制,但是 sprite 默认启用缓存,可能会拉低渲染效率
 | 
			
		||||
<sprite render={render} />;
 | 
			
		||||
 | 
			
		||||
// ✅ 好的写法,使用 nocache 标识禁用 sprite 的缓存机制
 | 
			
		||||
<sprite render={render} nocache />;
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## 特殊场景禁用抗锯齿和高清
 | 
			
		||||
 | 
			
		||||
默认情况下,大部分元素都是默认启用高清即抗锯齿的(`layer` 和 `layer-group` `icon` 不启用),这可能会导致一些不必要的计算出现,从而拉低渲染性能。对于一些需要保持像素风的内容,我们建议关闭抗锯齿和高清画布。代码示例如下:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
// ❌ 差的写法,像素风图片使用默认设置,启用了抗锯齿和高清
 | 
			
		||||
<image image="pixel.png" />
 | 
			
		||||
// ✅ 好的写法,关闭了默认的抗锯齿和高清
 | 
			
		||||
<image image="pixel.png" noanti hd={false} />
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## 在合适场景下隐藏一些元素
 | 
			
		||||
 | 
			
		||||
如果一个元素在某些场景下需要隐藏,另一些场景下需要显示,我们建议使用 `hidden` 属性来设置,而不是通过把它移动到画面外、调成透明颜色、使用 `if` 或三元表达式判断等方式。示例代码如下:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
// ❌ 差的写法,使用条件表达式切换元素显示与否
 | 
			
		||||
{
 | 
			
		||||
    !hidden.value && <sprite />;
 | 
			
		||||
}
 | 
			
		||||
// ✅ 好的写法,使用 hidden 属性
 | 
			
		||||
<sprite hidden={hidden.value} />;
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## 后续计划
 | 
			
		||||
 | 
			
		||||
我们后续计划推出渲染树调试工具,届时可以更加细致方便地查看渲染树的渲染情况以及性能问题。
 | 
			
		||||
@ -0,0 +1,243 @@
 | 
			
		||||
# UI 系统
 | 
			
		||||
 | 
			
		||||
本节将会讲解 2.B 的渲染树与 UI 系统的工作原理,以及一些常用 API。
 | 
			
		||||
 | 
			
		||||
## 创建一个自己的 UI 管理器
 | 
			
		||||
 | 
			
		||||
样板提供 `UIController` 类,允许你在自己的一个 UI 中创建自己的 UI 管理器,例如在样板中,游戏画面本身包含一个 UI 管理器,分为了封面、加载界面、游戏界面三种,其中游戏界面里面还有一个游戏 UI 管理器,我们常用的就是最后一个游戏 UI 管理器。
 | 
			
		||||
 | 
			
		||||
### 创建 UIController 实例
 | 
			
		||||
 | 
			
		||||
我们从 `@motajs/system-ui` 引入 `UIController` 类,然后对其实例化:
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
import { UIController } from '@motajs/system-ui';
 | 
			
		||||
 | 
			
		||||
// 传入一个字符串来表示这个控制器的 id
 | 
			
		||||
export const myController = new UIController('my-controller');
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### 获取 UI 控制器
 | 
			
		||||
 | 
			
		||||
可以通过 id 来获取到这个控制器,或者直接引入对应文件中的控制器:
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
import { UIController } from '@motajs/system-ui';
 | 
			
		||||
import { myController } from './myController';
 | 
			
		||||
 | 
			
		||||
const myController = UIController.get('my-controller');
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### 添加到渲染树
 | 
			
		||||
 | 
			
		||||
接下来,可以直接调用 `myController.render` 方法来添加到你自己的 UI 中:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
<container>{myController.render()}</container>
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## UI 显示模式
 | 
			
		||||
 | 
			
		||||
### 内置显示模式
 | 
			
		||||
 | 
			
		||||
UI 管理器内置了两种显示模式,只显示最后一个以及显示所有。其中前者常用于级联式 UI,例如 `设置 -> 系统设置 -> 快捷键设置`,这时候只会显示最后一个 UI,前面的 UI 不会显示。后者常用于展示信息类的 UI,例如在地图上展示怪物信息等。我们可以通过下面这两个方法来设置 UI 显示模式,立即生效,但不推荐频繁切换,建议一个控制器只使用**一种**显示模式:
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
// 设置为只显示最后一个
 | 
			
		||||
myController.lastOnly();
 | 
			
		||||
// 设置为显示所有
 | 
			
		||||
myController.showAll();
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### 栈模式
 | 
			
		||||
 | 
			
		||||
对于级联式 UI,我们希望在关闭一个 UI 时,在其之后的 UI 也能关闭,例如对于上面提到的 `设置 -> 系统设置 -> 快捷键设置` 级联 UI,当我们关闭设置界面时,我们会希望系统设置和快捷键设置也一并关闭,而不是需要手动关闭。这时候,栈模式就可以做到这一点,启用栈模式时,关闭一个 UI 后,在其之后的 UI 也会全部关闭。我们依然可以使用上面两个方法来设置是否启用栈模式:
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
// 设置为显示最后一个,启用栈模式,不过 lastOnly 默认启用栈模式,因此参数可不填
 | 
			
		||||
myController.lastOnly(true);
 | 
			
		||||
// 设置为显示最后一个,不启用栈模式
 | 
			
		||||
myController.lastOnly(false);
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### 自定义显示模式
 | 
			
		||||
 | 
			
		||||
::: info
 | 
			
		||||
这一小节内容不重要,没有特殊需求的可以不看。
 | 
			
		||||
:::
 | 
			
		||||
 | 
			
		||||
样板内置的两个显示模式以及栈模式已经能够满足绝大多数情况,不过可能还会有一些非常特殊的情况满足不了,这时候我们可以使用 `showCustom` 方法来自定义一个显示模式。这个方法要求传入一个参数,参数需要是 `IUICustomConfig` 对象,对象要求实现 `open` `close` `hide` `show` `update` 五个方法,我们来介绍一下如何做出一个自定义显示模式。
 | 
			
		||||
 | 
			
		||||
方法说明如下:
 | 
			
		||||
 | 
			
		||||
-   `open` 方法会在一个 UI 打开时调用,例如默认的 `lastOnly` 模式其实就是在打开 UI 时将 UI 添加至栈末尾,然后隐藏在其之前的所有 UI
 | 
			
		||||
-   `close` 方法会在一个 UI 关闭时调用,例如默认的 `lastOnly` 模式就会在这个时候把在传入 UI 之后的所有 UI 一并关闭
 | 
			
		||||
-   `hide` 方法会在一个 UI 隐藏时调用,默认的 `lastOnly` 模式会在这个时候把 UI 隐藏显示
 | 
			
		||||
-   `show` 方法会在一个 UI 显示时调用,默认的 `lastOnly` 模式会在这个时候把 UI 启用显示
 | 
			
		||||
-   `update` 方法会在切换显示模式时调用,默认的 `lastOnly` 模式会在这个时候把最后一个 UI 显示,之前的隐藏
 | 
			
		||||
 | 
			
		||||
那么,假如我们要做一个反向 `lastOnly`,即只显示第一个,添加 UI 时添加至队列开头,我们可以这么写:
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
import { IUICustomConfig, IUIInstance } from '@motajs/system-ui';
 | 
			
		||||
 | 
			
		||||
const myCustomMode: IUICustomConfig = {
 | 
			
		||||
    open(ins: IUIInstance, stack: IUIInstance[]) {
 | 
			
		||||
        stack.forEach(v => v.hide()); // 隐藏当前所有 UI
 | 
			
		||||
        stack.unshift(ins); // 将要打开的 UI 添加至队列开头
 | 
			
		||||
        ins.show(); // 显示要打开的 UI
 | 
			
		||||
    },
 | 
			
		||||
    close(ins: IUIInstance, stack: IUIInstance[], index: number) {
 | 
			
		||||
        stack.splice(0, index + 1); // 关闭传入 UI 及其之前的所有内容
 | 
			
		||||
        stack[0]?.show(); // 显示第一个 UI
 | 
			
		||||
    },
 | 
			
		||||
    hide(ins: IUIInstance, stack: IUIInstance[], index: number) {
 | 
			
		||||
        ins.hide(); // 直接隐藏
 | 
			
		||||
    },
 | 
			
		||||
    show(ins: IUIInstance, stack: IUIInstance[], index: number) {
 | 
			
		||||
        ins.show(); // 直接显示
 | 
			
		||||
    },
 | 
			
		||||
    update(stack: IUIInstance[]) {
 | 
			
		||||
        stack.forEach(v => v.hide()); // 先隐藏所有 UI
 | 
			
		||||
        stack[0]?.show(); // 然后显示第一个 UI
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
myController.showCustom(myCustomMode); // 应用自己的显示模式
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## 设置 UI 背景
 | 
			
		||||
 | 
			
		||||
我们可以为 UI 设置背景组件,背景组件在 UI 打开时常亮。我们推荐使用此方法来为 UI 设置背景,因为它可以搭配 `keep` 防抖动来使用,避免出现 UI 闪烁的问题。现在,我们使用样板内置的 `Background` 背景组件作为例子,来展示如何设置背景:
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
import { Background } from '@user/client-modules';
 | 
			
		||||
 | 
			
		||||
// 传入背景组件作为背景,然后设置参数,使用 winskin.png 作为背景
 | 
			
		||||
myController.setBackground(Background, { winskin: 'winskin.png' });
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
默认情况下,当我们打开 UI 时,背景组件将会自动展示,不过我们也可以手动控制背景组件是否显示,它的优先级高于系统优先级:
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
myController.hideBackground(); // 隐藏背景组件,即使有 UI 已经打开,也不会显示背景
 | 
			
		||||
myController.showBackground(); // 显示背景组件,在 UI 已经打开的情况下展示,没有 UI 打开时不显示
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## 背景维持防抖动
 | 
			
		||||
 | 
			
		||||
有时候,我们需要关闭当前 UI 然后立刻打开下一个 UI,例如使用一个道具时可能会打开一个新的页面,这时候会先关闭道具背包界面,再打开道具的页面,这时候可能会出现短暂的“背景丢失”,这是因为 UI 的挂载需要时间,在极短的时间内如果没有挂载上,那么就会在屏幕上什么都不显示,上面设置的背景 UI 也不会显示,会引起一次闪烁,观感很差。为了解决这个问题,我们提供了背景维持防抖动的功能,使用 `keep` 方法来实现:
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
const keep = myController.keep();
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
调用此方法后,在下一次 UI 全部关闭时,背景会暂时维持,直到有 UI 打开,也就是说它会维持一次 UI 背景不会关闭,下一次就失效了。这样的话,如果我们去使用一个打开页面的道具,就不会出现闪烁的问题了。不过,假如我们使用了一个没有打开页面的道具,会有什么表现?答案是背景一直显示着,用户就什么也干不了了,这显然不是我们希望的,因此 `keep` 函数的返回值提供了一些能力来让你关闭背景,它们包括:
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
// 推荐方法,使用 safelyUnload 安全地卸载背景,这样如果有 UI 已经打开,不会将其关闭
 | 
			
		||||
keep.safelyUnload();
 | 
			
		||||
// 不推荐方法,调用后立刻关闭所有 UI,不常用
 | 
			
		||||
keep.unload();
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## 打开与关闭 UI
 | 
			
		||||
 | 
			
		||||
在 UI 编写章节已经提到了打开和关闭 UI 使用 `open` 和 `close` 方法,现在我们更细致地讲解一下如何打开与关闭 UI。打开 UI 使用 `open` 方法,定义如下:
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
function open<T extends UIComponent>(
 | 
			
		||||
    ui: IGameUI<T>,
 | 
			
		||||
    props: UIProps<T>,
 | 
			
		||||
    alwaysShow?: boolean
 | 
			
		||||
): IUIInstance;
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
其中第一个参数表示要打开的 UI,第二个表示传给 UI 的参数,第三个表示 UI 是否永远保持显示状态(除非被关闭),不受到显示模式的影响。同种 UI 可以打开多个,也可以在不同的控制器上同时打开多个相同的 UI。例如,如果我们想在主 UI 控制器中添加一个常量的返回游戏按钮,就可以这么写:
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
// BackToGame 是自定义 UI,第三个参数传 true 来保证它一直显示在画面上
 | 
			
		||||
myController.open(BackToGame, {}, true);
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
关闭 UI 使用 `close` 方法,传入 UI 实例,即 `open` 方法的返回值,没有其他参数。例如:
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
const MyUI = defineComponent(props => {
 | 
			
		||||
    // 所有通过 UI 控制器打开的,同时按照 UI 模板填写了 props 的 UI 都包含 controller 和 instance 属性
 | 
			
		||||
    props.controller.close(props.instance);
 | 
			
		||||
}, myUIProps);
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
除此之外,还提供了一个关闭所有 UI 的:
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
function closeAll(ui?: IGameUI): void;
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
其中参数表示要关闭的 UI 类型,不填时表示关闭所有 UI,填写时表示关闭所有指定类型的 UI。例如我想关闭所有 `EnemyInfo` UI,可以这么写:
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
// EnemyInfo 是自定义 UI
 | 
			
		||||
myController.closeAll(EnemyInfo);
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## 渲染系统的树结构
 | 
			
		||||
 | 
			
		||||
接下来我们来讲解一下渲染系统的一些工作原理。下面的部分由 `DeepSeek R1` 模型生成并稍作修改。
 | 
			
		||||
 | 
			
		||||
### 结构原理
 | 
			
		||||
 | 
			
		||||
想象一棵倒着生长的树:
 | 
			
		||||
 | 
			
		||||
-   根节点:相当于画布本身,是所有元素的起点
 | 
			
		||||
-   枝干节点:类似文件夹,可以包含其他元素
 | 
			
		||||
-   叶子节点:实际显示的内容,如图片、文字等
 | 
			
		||||
 | 
			
		||||
### 运作特点
 | 
			
		||||
 | 
			
		||||
-   层级管理:子元素永远在父元素的"内部"显示
 | 
			
		||||
-   自动排序:像叠扑克牌一样,后添加的元素默认盖在之前元素上方,不过也可以通过参数来调整顺序
 | 
			
		||||
-   智能裁剪:父元素就像相框,超出范围的内容自动隐藏
 | 
			
		||||
 | 
			
		||||
## 渲染系统的事件系统
 | 
			
		||||
 | 
			
		||||
### 事件传递三阶段
 | 
			
		||||
 | 
			
		||||
1. 收件扫描(捕获阶段):从根部开始层层扫描,寻找可能接收事件的元素,类似快递分拣中心扫描包裹目的地
 | 
			
		||||
2. 精准投递(目标阶段):找到实际触发事件的元素进行处理,就像快递员将包裹送到收件人手中
 | 
			
		||||
3. 回执确认(冒泡阶段):处理结果沿着原路返回汇报,如同收件人签收后系统更新物流状态
 | 
			
		||||
 | 
			
		||||
将事件分为三个阶段,是为了让交互更加符合直觉,你也不想点击内层按钮的时候外层按钮也被触发吧)
 | 
			
		||||
 | 
			
		||||
### 特殊处理机制
 | 
			
		||||
 | 
			
		||||
-   紧急拦截:任何环节都可以标记"无需继续传递"
 | 
			
		||||
-   批量处理:多个事件自动合并减少处理次数
 | 
			
		||||
-   智能过滤:自动忽略不可见区域的事件
 | 
			
		||||
 | 
			
		||||
## 冒泡更新
 | 
			
		||||
 | 
			
		||||
### 工作原理
 | 
			
		||||
 | 
			
		||||
当某个元素发生变化时:自动通知直系父元素,父元素检查自身是否需要调整,继续向上传递直到根部,最终统一计算所有需要改变的位置,并在下一帧执行更新。
 | 
			
		||||
 | 
			
		||||
### 设计优势
 | 
			
		||||
 | 
			
		||||
-   精准定位:只更新受影响的部分画面
 | 
			
		||||
-   避免重复:多个子元素变化只需一次整体计算
 | 
			
		||||
-   顺序保障:始终从最深层开始逐层处理
 | 
			
		||||
 | 
			
		||||
## 懒更新机制
 | 
			
		||||
 | 
			
		||||
### 工作模式
 | 
			
		||||
 | 
			
		||||
1. 收集阶段:记录所有需要改变的内容(如颜色变化、文字修改)
 | 
			
		||||
2. 等待时机:一般是等待到下一帧
 | 
			
		||||
3. 批量处理:一次性完成所有修改
 | 
			
		||||
 | 
			
		||||
### 实际效益
 | 
			
		||||
 | 
			
		||||
-   性能优化:减少像频繁开关灯的资源浪费
 | 
			
		||||
-   流畅保障:避免连续小改动导致的画面闪烁
 | 
			
		||||
-   智能调度:优先处理用户可见区域的变化
 | 
			
		||||
							
								
								
									
										850
									
								
								docs/guide/ui.md
									
									
									
									
									
								
							
							
						
						
									
										850
									
								
								docs/guide/ui.md
									
									
									
									
									
								
							@ -0,0 +1,850 @@
 | 
			
		||||
---
 | 
			
		||||
lang: zh-CN
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
# UI 编写
 | 
			
		||||
 | 
			
		||||
本文将介绍如何在 2.B 样板中编写 UI,以及如何优化 UI 性能。
 | 
			
		||||
 | 
			
		||||
## 创建 UI 文件
 | 
			
		||||
 | 
			
		||||
首先,我们打开 `packages-user/client-modules/render` 文件夹,这里是目前样板的 UI 目录(之后可能会修改),我们可以看到 `components` `legacy` `ui` 三个文件夹,其中 `component` 是组件文件夹,也就是所有 UI 都可能用到的组件,例如滚动条、分页、图标等,这些东西不会单独组成一个 UI,但是可以方便 UI 开发。`legacy` 文件夹是将要删除或重构的内容,不建议使用里面的内容。`ui` 就是 UI 文件夹,这里面存放了所有的 UI,我们在这里创建一个文件 `myUI.tsx`。
 | 
			
		||||
 | 
			
		||||
## 编写 UI 模板
 | 
			
		||||
 | 
			
		||||
下面,我们需要编写 UI 模板,以怪物手册为例,模板如下,直接复制粘贴即可:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { GameUI, UIComponentProps } from '@motajs/system-ui';
 | 
			
		||||
import { SetupComponentOptions } from '../components';
 | 
			
		||||
 | 
			
		||||
export interface MyBookProps extends UIComponentProps {}
 | 
			
		||||
 | 
			
		||||
const myBookProps = {
 | 
			
		||||
    props: ['controller', 'instance']
 | 
			
		||||
} satisfies SetupComponentOptions<MyBookProps>;
 | 
			
		||||
 | 
			
		||||
export const MyBook = defineComponent<MyBookProps>(props => {
 | 
			
		||||
    return () => <container></container>;
 | 
			
		||||
}, myBookProps);
 | 
			
		||||
 | 
			
		||||
export const MyBookUI = new GameUI('my-book', MyBook);
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
然后打开 `index.ts`,增加如下代码:
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
export * from './myUI';
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## 添加一些内容
 | 
			
		||||
 | 
			
		||||
新的 UI 使用 tsx 编写,即 `TypeScript JSX`,可以直接在 ts 文件中编写 XML,非常适合编写 UI。例如,我们想要把 UI 的位置设为水平竖直居中,位置在 240, 240,长宽为 480, 480,并显示一个文字,可以这么写:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
// ... 其他内容
 | 
			
		||||
// loc 参数表示这个元素的位置,六个数分别表示:
 | 
			
		||||
// 横纵坐标;长宽;水平竖直锚点,0.5 表示居中,1 表示靠右或靠下对齐,可以填不在 0-1 范围的数
 | 
			
		||||
// 每两项组成一组,这两项要么都填,要么都不填,例如长宽可以都不填,横纵坐标可以都不填
 | 
			
		||||
// 不填时会使用默认值,或是组件内部计算出的值
 | 
			
		||||
return () => (
 | 
			
		||||
    <container loc={[240, 240, 480, 480, 0.5, 0.5]}>
 | 
			
		||||
        {/* 文字元素会自动计算长宽,因此不能手动指定 */}
 | 
			
		||||
        <text text="这是一段文字" loc={[240, 240, void 0, void 0, 0.5, 0.5]} />
 | 
			
		||||
    </container>
 | 
			
		||||
);
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## 显示 UI
 | 
			
		||||
 | 
			
		||||
我们编写完 UI 之后,这个 UI 并不会自己显示,需要手动打开。我们找到 `ui/main.tsx`,在 `MainScene` 这个根组件中添加一句话:
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
// 在这添加引入
 | 
			
		||||
import { MyBookUI } from './ui';
 | 
			
		||||
// ... 其他内容
 | 
			
		||||
const MainScene = defineComponent(() => {
 | 
			
		||||
    // ... 其他内容
 | 
			
		||||
    // 在这添加一句话,打开 UI,第二个参数为传入 UI 的参数,后面会有讲解
 | 
			
		||||
    // 纵深设为 100 以保证可以显示出来,纵深越大,元素越靠上,会覆盖纵深低的元素
 | 
			
		||||
    mainUIController.open(MyBookUI, { zIndex: 100 });
 | 
			
		||||
    return () => (
 | 
			
		||||
        // ... 其他内容
 | 
			
		||||
    );
 | 
			
		||||
});
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
这样的话,我们就会在页面上显示一个新的 UI 了!不过这个 UI 会是常亮的 UI,没办法关闭,我们需要更精细的控制。我们可以在内部使用 `props.controller` 来获取到 UI 控制器实例,使用 `props.instance` 获取到当前 UI 实例,从而控制当前 UI 的状态:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
export const MyBook = defineComponent<MyBookProps>(props => {
 | 
			
		||||
    // 例如,我们可以让它在打开 10 秒钟后关闭:
 | 
			
		||||
    setTimeout(() => props.controller.close(props.instance), 10000);
 | 
			
		||||
    return () => (
 | 
			
		||||
        // ... UI 内容
 | 
			
		||||
    );
 | 
			
		||||
}, myBookProps);
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
除此之外,我们还可以在任意渲染端模块中引入 `ui/controller` 来获取到根组件的 UI 控制器,注意跨文件夹引入时需要引入 `@user/client-modules`。例如,我们可以在其他文件中控制这个 UI 的开启与关闭:
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
import { mainUIController, MyBookUI } from './ui';
 | 
			
		||||
import { IUIInstance } from '@motajs/system-ui';
 | 
			
		||||
 | 
			
		||||
let myBookInstance: IUIInstance;
 | 
			
		||||
export function openMyBook() {
 | 
			
		||||
    // 使用一个变量来记录打开的 UI 实例
 | 
			
		||||
    myBookInstance = mainUIController.open(MyBookUI, {});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function closeMyBook() {
 | 
			
		||||
    // 传入 UI 实例,将会关闭此 UI 及其之后的 UI
 | 
			
		||||
    mainUIController.close(myBookInstance);
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
也可以使用 `Mota.require` 引入:
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
const { mainUIController } = Mota.require('@user/client-modules');
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
也可以通过 `UIController` 的接口获取其实例:
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
import { UIController } from '@motajs/system-ui';
 | 
			
		||||
 | 
			
		||||
const mainUIController = UIController.getController('main-ui');
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
更多的 UI 控制功能可以参考后续文档以及相关的 [UI 系统指南](./ui-system.md) 或 [API 文档](../api/motajs-system-ui/UIController)。
 | 
			
		||||
 | 
			
		||||
## 添加更多内容
 | 
			
		||||
 | 
			
		||||
既然我们要编写一个简易怪物手册,那么仅靠上面这些内容当然不够,我们需要更多的元素和组件才行,下面我们来介绍一些常用的元素及组件。
 | 
			
		||||
 | 
			
		||||
### 图标
 | 
			
		||||
 | 
			
		||||
既然是怪物手册,那么图标必然不能少,图标是 `<icon>` 元素,需要传入 `icon` 参数,例如:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
return () => (
 | 
			
		||||
    <container>
 | 
			
		||||
        {/* 显示绿史莱姆图标,位置在 (32, 32),循环播放动画 */}
 | 
			
		||||
        <icon icon="greenSlime" loc={[32, 32]} animate />
 | 
			
		||||
    </container>
 | 
			
		||||
);
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### 字体
 | 
			
		||||
 | 
			
		||||
我们很多时候也会想要自定义字体,可以通过 `Font` 类来实现这个功能:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
import { Font, FontWeight } from '@motajs/render';
 | 
			
		||||
 | 
			
		||||
// 创建一个字体,包含五个参数,第一个是字体名称,第二个是字体大小,第三个是字体大小的单位,一般是 'px'
 | 
			
		||||
// 第四个是字体粗细,默认是 400,可以填 FontWeight.Bold,FontWeight.Light 或是数字,范围在 1-1000 之间
 | 
			
		||||
// 第五个是是否斜体。每个参数都是可选,不填则使用默认字体的样式。
 | 
			
		||||
const font = new Font('myFont', 24, 'px', FontWeight.Bold, false);
 | 
			
		||||
// 可以将这个字体设置为默认字体,之后的所有没有指定的都会使用此字体
 | 
			
		||||
Font.setDefaults(font);
 | 
			
		||||
// 如果需要使用默认字体,有两种写法
 | 
			
		||||
const font = new Font();
 | 
			
		||||
const font = Font.defaults();
 | 
			
		||||
 | 
			
		||||
return () => (
 | 
			
		||||
    <container>
 | 
			
		||||
        <icon icon="greenSlime" loc={[32, 32]} animate />
 | 
			
		||||
        <text
 | 
			
		||||
            text="绿史莱姆"
 | 
			
		||||
            // 使用上面定义的字体
 | 
			
		||||
            font={font}
 | 
			
		||||
            // 靠左对齐,上下居中对齐
 | 
			
		||||
            loc={[64, 48, void 0, void 0, 0, 0.5]}
 | 
			
		||||
        />
 | 
			
		||||
    </container>
 | 
			
		||||
);
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
更多的字体使用方法可以参考 [API 文档](../api/motajs-render-style/Font)
 | 
			
		||||
 | 
			
		||||
### 圆角矩形
 | 
			
		||||
 | 
			
		||||
我们可以为怪物手册的一栏添加圆角矩形,写法如下:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
return () => (
 | 
			
		||||
    <container>
 | 
			
		||||
        <g-rectr
 | 
			
		||||
            // 圆角矩形的位置
 | 
			
		||||
            loc={[16, 16, 480 - 32, 480 - 32]}
 | 
			
		||||
            // 圆角矩形为仅描边
 | 
			
		||||
            stroke
 | 
			
		||||
            // 圆角半径,可以设置四个,具体参考圆角矩形的文档
 | 
			
		||||
            circle={[8]}
 | 
			
		||||
            // 描边样式,这里设为了金色
 | 
			
		||||
            strokeStyle="gold"
 | 
			
		||||
        />
 | 
			
		||||
    </container>
 | 
			
		||||
);
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### 线段
 | 
			
		||||
 | 
			
		||||
我们也可以添加线段,作为怪物列表之间的分割线:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
return () => (
 | 
			
		||||
    <container>
 | 
			
		||||
        <g-line
 | 
			
		||||
            // 线段的起始位置和终止位置,不需要指定 loc 属性
 | 
			
		||||
            line={[16, 80, 480 - 16, 80]}
 | 
			
		||||
            // 线的端点为圆形
 | 
			
		||||
            lineCap="round"
 | 
			
		||||
            // 线宽为 1
 | 
			
		||||
            lineWidth={1}
 | 
			
		||||
            // 虚线样式,5 个像素为实,5 个像素为虚
 | 
			
		||||
            lineDash={[5, 5]}
 | 
			
		||||
        />
 | 
			
		||||
    </container>
 | 
			
		||||
);
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### winskin 背景
 | 
			
		||||
 | 
			
		||||
我们可以为手册添加一个 winskin 背景,可以使用 `Background` 组件:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
// 从 components 文件夹中引入这个组件
 | 
			
		||||
import { Background } from '../components';
 | 
			
		||||
 | 
			
		||||
return () => (
 | 
			
		||||
    <container loc={[240, 240, 480, 480, 0.5, 0.5]}>
 | 
			
		||||
        <Background
 | 
			
		||||
            // 位置是相对于父元素的,因此从 (0, 0) 开始
 | 
			
		||||
            loc={[0, 0, 480, 480]}
 | 
			
		||||
            // 设置 winskin 的图片名称
 | 
			
		||||
            winskin="winskin.png"
 | 
			
		||||
        />
 | 
			
		||||
    </container>
 | 
			
		||||
);
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### 滚动条
 | 
			
		||||
 | 
			
		||||
怪物多的话一页肯定显示不完,因此我们可以添加一个滚动条 `Scroll` 组件,用法如下:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
// 从 components 文件夹中引入这个组件
 | 
			
		||||
import { Scroll } from '../components';
 | 
			
		||||
 | 
			
		||||
return () => (
 | 
			
		||||
    // 使用滚动条组件替换 container 元素
 | 
			
		||||
    <Scroll loc={[240, 240, 480, 480, 0.5, 0.5]}> // [!code ++]
 | 
			
		||||
        <Background
 | 
			
		||||
            // 位置是相对于父元素的,因此从 (0, 0) 开始
 | 
			
		||||
            loc={[0, 0, 480, 480]}
 | 
			
		||||
            // 设置 winskin 的图片名称
 | 
			
		||||
            winskin="winskin.png"
 | 
			
		||||
        />
 | 
			
		||||
        {/* 其他内容 */}
 | 
			
		||||
    </Srcoll> // [!code ++]
 | 
			
		||||
);
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
在使用滚动条时,建议使用平铺式布局,将每个独立的内容平铺显示,而不是整体包裹为一个 `container`,这有助于提高性能表现。
 | 
			
		||||
 | 
			
		||||
### 循环
 | 
			
		||||
 | 
			
		||||
编写怪物手册的话,我们就必须用到循环,因为我们需要遍历当前怪物列表,然后每个怪物生成一个 `container`,在这个 `container` 里面显示内容。tsx 为我们提供了嵌入表达式的功能,因此我们可以通过 `map` 方法来遍历怪物列表,然后返回一个元素,组成元素数组,实现循环遍历的功能。示例如下:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
export const MyBook = defineComponent<MyBookProps>(props => {
 | 
			
		||||
    // 获取怪物列表,enemys 为 CurrenEnemy 数组,可以查看 package-user/data-fallback/src/battle.ts
 | 
			
		||||
    const enemys = core.getCurrentEnemys();
 | 
			
		||||
    // 工具函数,居中,靠右,靠左对齐文字
 | 
			
		||||
    const central = (x: number, y: number) => [x, y, void 0, void 0, 0.5, 0.5];
 | 
			
		||||
    const right = (x: number, y: number) => [x, y, void 0, void 0, 1, 0.5];
 | 
			
		||||
    const left = (x: number, y: number) => [x, y, void 0, void 0, 0, 0.5];
 | 
			
		||||
 | 
			
		||||
    return () => (
 | 
			
		||||
        <Scroll>
 | 
			
		||||
            {/* 写一个 map 循环,将一个容器元素返回,就可以显示了 */}
 | 
			
		||||
            {enemys.map((v, i) => {
 | 
			
		||||
                return (
 | 
			
		||||
                    <container loc={[0, 80 * i, 480, 80]}>
 | 
			
		||||
                        {/* 怪物图标与怪物名称 */}
 | 
			
		||||
                        <icon icon={v.enemy.id} loc={[32, 16, 32, 32]} />
 | 
			
		||||
                        <text text={v.enemy.enemy.name} loc={central(48, 64)} />
 | 
			
		||||
                        {/* 显示怪物的属性 */}
 | 
			
		||||
                        <text text="生命" loc={right(96, 20)} />
 | 
			
		||||
                        <text text={v.enemy.info.hp} loc={left(108, 20)} />
 | 
			
		||||
                        {/* 其他的属性,例如攻击,防御等 */}
 | 
			
		||||
                    </container>
 | 
			
		||||
                );
 | 
			
		||||
            })}
 | 
			
		||||
        </Scroll>
 | 
			
		||||
    );
 | 
			
		||||
}, myBookProps);
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### 条件判断
 | 
			
		||||
 | 
			
		||||
可以在表达式中使用三元表达式或者立即执行函数来实现条件判断:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
return () => (
 | 
			
		||||
    <Scroll>
 | 
			
		||||
        {enemys.length === 0 ? (
 | 
			
		||||
            // 无怪物时,显示没有剩余怪物
 | 
			
		||||
            <text text="没有剩余怪物" loc={central(240. 240)} font={new Font('Verdana', 48)} /> // [!code ++]
 | 
			
		||||
        ) : (
 | 
			
		||||
            enemys.map(v => {
 | 
			
		||||
                // 有怪物时
 | 
			
		||||
            })
 | 
			
		||||
        )}
 | 
			
		||||
    </Scroll>
 | 
			
		||||
);
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## 响应式
 | 
			
		||||
 | 
			
		||||
使用新的 UI 系统时,最大的优势就是响应式了,它可以让 UI 在数据发生变动时自动更改显示内容,而不需要手动重绘。本 UI 系统完全兼容 `vue` 的响应式系统,非常方便。
 | 
			
		||||
 | 
			
		||||
### 基础用法
 | 
			
		||||
 | 
			
		||||
例如,我想要给我的怪物手册添加一个楼层 id 的参数,首先我们先定义这个参数:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
import { computed } from 'vue';
 | 
			
		||||
 | 
			
		||||
export interface MyBookProps extends UIComponentProps {
 | 
			
		||||
    // 定义 floorId 参数
 | 
			
		||||
    floorId: FloorIds;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const myBookProps = {
 | 
			
		||||
    // 这里也要修改
 | 
			
		||||
    props: ['controller', 'instance', 'floorId']
 | 
			
		||||
} satisfies SetupComponentOptions<MyBookProps>;
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
然后我们需要在这个参数发生变动时修改怪物列表,可以这么写:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
export const MyBook = defineComponent<MyBookProps>(props => {
 | 
			
		||||
    // 使用 computed,这样的话就会自动追踪到 props.floorId 参数,更新怪物列表,并更新显示内容
 | 
			
		||||
    const enemys = computed(() => core.getCurrentEnemys(props.floorId)); // [!code ++]
 | 
			
		||||
 | 
			
		||||
    return () => (
 | 
			
		||||
        <Scroll>
 | 
			
		||||
            {/* 需要使用 enemys.value 属性,不能直接使用 enemys.length */}
 | 
			
		||||
            {enemys.value.length === 0 ? ( // [!code ++]
 | 
			
		||||
                <text text="没有剩余怪物" loc={central(240. 240)} font={new Font('Verdana', 48)} />
 | 
			
		||||
            ) : (
 | 
			
		||||
                // 同上,需要 value 属性
 | 
			
		||||
                enemys.value.map(v => {}) // [!code ++]
 | 
			
		||||
            )}
 | 
			
		||||
        </Scroll>
 | 
			
		||||
    );
 | 
			
		||||
}, myBookProps);
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### 什么样的变量能使用响应式
 | 
			
		||||
 | 
			
		||||
其实,我们用一般的方式编写的变量或常量都是不能使用响应式的,例如这些都不行:
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
let num = 10;
 | 
			
		||||
let str = '123';
 | 
			
		||||
 | 
			
		||||
const num2 = computed(() => num * 2);
 | 
			
		||||
const str2 = computed(() => parseInt(str));
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
这么写的话,是没有响应式效果的,这是因为 `num` 和 `str` 并不是响应式变量,不能追踪到。对于 `string` `number` `boolean` 这些字面量类型的变量,我们需要使用 `ref` 函数包裹才可以:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
import { ref } from 'vue';
 | 
			
		||||
 | 
			
		||||
// 使用 ref 函数包裹
 | 
			
		||||
const num = ref(10);
 | 
			
		||||
// 使用 num.value 属性调用
 | 
			
		||||
const num2 = computed(() => num.value * 2);
 | 
			
		||||
// 使用 num.value 修改值
 | 
			
		||||
num.value = 20;
 | 
			
		||||
 | 
			
		||||
// 这样的话就有响应式效果了
 | 
			
		||||
<text text={num2.value.toString()} />;
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
对于对象类型来说,需要使用 `reactive` 函数包裹,这个函数会把对象变成深层响应式,任何一级发生更改都会触发响应式更新,例如:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
const obj = reactive({ obj1: { num: 10 } });
 | 
			
		||||
 | 
			
		||||
// 这个就不需要使用 value 属性了,只有 ref 函数包裹的需要
 | 
			
		||||
obj.obj1.num = 20;
 | 
			
		||||
 | 
			
		||||
// 直接调用即可,当值更改时内容也会自动更新
 | 
			
		||||
<text text={obj.obj1.num.toString()} />;
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
数组也可以使用 `reactive` 方法来实现响应式:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
// 传入一个泛型来指定这个变量的类型,这里使用数字数组作为示例
 | 
			
		||||
const array = reactive<number[]>([]);
 | 
			
		||||
 | 
			
		||||
// 可以使用数组自身的方法添加或修改元素
 | 
			
		||||
array.push(100);
 | 
			
		||||
 | 
			
		||||
<container>
 | 
			
		||||
    {/* 直接对数组遍历,数组修改后这段内容也会自动更新 */}
 | 
			
		||||
    {array.map(v => (
 | 
			
		||||
        <text text={v.toString()} />
 | 
			
		||||
    ))}
 | 
			
		||||
</container>;
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
如果对象比较大,只想让第一层变为响应式,深层的不变,可以使用 `shallowReactive` 或 `shallowRef`,或使用 `markRaw` 手动标记不需要响应式的部分:
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
// 这样的话,当 obj1.obj1.num 修改时,就不会触发响应式,而 obj1.obj1 修改时会触发
 | 
			
		||||
const obj1 = shallowReactive({ obj1: { num: 10 } });
 | 
			
		||||
// 使用 shallowRef,也可以变成浅层响应式
 | 
			
		||||
const obj2 = shallowRef({ obj1: { num: 10 } });
 | 
			
		||||
// 或者手动标记为不需要响应式
 | 
			
		||||
const obj3 = reactive({ obj1: markRaw({ num: 10 }) });
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
响应式不仅可以用在 `computed` 或者是渲染元素中,还可以使用 `watch` 监听。不过该方法有一定的限制,那就是尽量不要在组件顶层之外使用。下面是一些例子:
 | 
			
		||||
 | 
			
		||||
::: code-group
 | 
			
		||||
 | 
			
		||||
```ts [ref]
 | 
			
		||||
const num1 = ref(10);
 | 
			
		||||
const num2 = ref(20);
 | 
			
		||||
 | 
			
		||||
watch(num1, (newValue, oldValue) => {
 | 
			
		||||
    // 当 num1 的值发生变化时,在控制台输出新值和旧值
 | 
			
		||||
    console.log(newValue, oldValue);
 | 
			
		||||
 | 
			
		||||
    // 这里就不是组件顶层,不要使用 watch。如果需要条件判断的话,可以在监听函数内部判断,而不是外部
 | 
			
		||||
    watch(num2, () => {});
 | 
			
		||||
});
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
```ts [reactive]
 | 
			
		||||
const obj = reactive({
 | 
			
		||||
    num: 10,
 | 
			
		||||
    obj1: {
 | 
			
		||||
        num2: 20
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 监听 obj.num
 | 
			
		||||
watch(
 | 
			
		||||
    () => obj.num,
 | 
			
		||||
    (newValue, oldValue) => {
 | 
			
		||||
        console.log(newValue, oldValue);
 | 
			
		||||
    }
 | 
			
		||||
);
 | 
			
		||||
// 监听 obj 整体
 | 
			
		||||
watch(obj, () => {
 | 
			
		||||
    console.log(obj.num);
 | 
			
		||||
});
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
:::
 | 
			
		||||
 | 
			
		||||
::: info
 | 
			
		||||
传入组件的 `props` 参数也是响应式的,可以通过 `watch` 监听,或使用 `computed` 追踪。
 | 
			
		||||
:::
 | 
			
		||||
 | 
			
		||||
关于更多 `vue` 响应式的知识,可以查看 [Vue 官方文档](https://cn.vuejs.org/)
 | 
			
		||||
 | 
			
		||||
## 鼠标与触摸交互事件
 | 
			
		||||
 | 
			
		||||
### 监听鼠标或触摸
 | 
			
		||||
 | 
			
		||||
通过上面这些内容,我们已经可以搭出来一个完整的怪物手册页面了,不过现在这个页面是死的,还没办法交互,我们需要让它有办法交互,允许用户点击和按键操作。UI 系统提供了丰富方便的接口来实现交互动作的监听,例如监听点击可以使用 `onClick`:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
const click = () => {
 | 
			
		||||
    console.log('clicked!');
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 直接将函数传入 onClick 属性即可
 | 
			
		||||
<container onClick={click}>{/* 渲染内容 */}</container>;
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
可以使用 `cursor` 属性来指定鼠标移动到该元素上时的指针样式,如下例所示,鼠标移动到这个容器上时就会变成小手的形状:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
<container cursor="pointer" />
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
鼠标与触摸事件的触发包括两个阶段,从根节点捕获,然后一路传递到最下层,然后从最下层冒泡,然后一路再传递回根节点,一般情况下我们使用冒泡阶段的监听即可,也就是 `onXxx`,例如 `onClick` 等,不过如果我们需要监听捕获阶段的事件,也可以使用 `onXxxCapture` 的方法来监听:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
const clickCapture = () => {
 | 
			
		||||
    console.log('click capture.');
 | 
			
		||||
};
 | 
			
		||||
const click = () => {
 | 
			
		||||
    console.log('click bubble.');
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
<container onClick={click} onClickCapture={clickCapture} />;
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
当点击这个容器时,就会先触发 `clickCapture` 事件,再触发 `click` 事件。
 | 
			
		||||
 | 
			
		||||
### 监听事件的类型
 | 
			
		||||
 | 
			
		||||
鼠标和触摸交互包含如下类型:
 | 
			
		||||
 | 
			
		||||
-   `click`: 当按下与抬起都发生在这个元素上时触发,冒泡阶段
 | 
			
		||||
-   `clickCapture`: 同上,捕获阶段
 | 
			
		||||
-   `down`: 当在这个元素上按下时触发,冒泡阶段
 | 
			
		||||
-   `downCapture`: 同上,捕获阶段
 | 
			
		||||
-   `up`: 当在这个元素上抬起时触发,冒泡阶段
 | 
			
		||||
-   `upCapture`: 同上,捕获阶段
 | 
			
		||||
-   `move`: 当在这个元素上移动时触发,冒泡阶段
 | 
			
		||||
-   `moveCapture`: 同上,捕获阶段
 | 
			
		||||
-   `enter`: 当进入这个元素时触发,顺序不固定,没有捕获阶段与冒泡阶段的分类
 | 
			
		||||
-   `leave`: 当离开这个元素时触发,顺序不固定,没有捕获阶段与冒泡阶段的分类
 | 
			
		||||
-   `wheel`: 当在这个元素上滚轮时触发,冒泡阶段
 | 
			
		||||
-   `wheelCapture`: 同上,捕获阶段
 | 
			
		||||
 | 
			
		||||
触发顺序如下,滚轮单独列出,不在下述顺序中:
 | 
			
		||||
 | 
			
		||||
1. `downCapture`,按下捕获
 | 
			
		||||
2. `down`: 按下冒泡
 | 
			
		||||
3. `moveCapture`: 移动捕获
 | 
			
		||||
4. `move`: 移动冒泡
 | 
			
		||||
5. `leave`: 离开元素
 | 
			
		||||
6. `enter`: 进入元素
 | 
			
		||||
7. `upCapture`: 抬起捕获
 | 
			
		||||
8. `up`: 抬起冒泡
 | 
			
		||||
9. `clickCapture`: 点击捕获
 | 
			
		||||
10. `click`: 点击冒泡
 | 
			
		||||
 | 
			
		||||
### 阻止事件传播
 | 
			
		||||
 | 
			
		||||
有时候我们需要阻止交互事件的继续传播,例如按钮套按钮时,我们不希望点击内部按钮时也触发外部按钮,这时候我们需要在内部按钮中阻止冒泡的继续传播。每个交互事件都可以接受一个参数,调用这个参数的 `stopPropagation` 方法即可阻止冒泡或捕获的继续传播:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
import { IActionEvent } from '@motajs/render';
 | 
			
		||||
 | 
			
		||||
const click1 = (e: IActionEvent) => {
 | 
			
		||||
    // 调用以阻止冒泡的继续传播
 | 
			
		||||
    e.stopPropagation();
 | 
			
		||||
    console.log('click1');
 | 
			
		||||
};
 | 
			
		||||
const click2 = () => {
 | 
			
		||||
    console.log('click2');
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
<container onClick={click2}>
 | 
			
		||||
    <container onClick={click1}></container>
 | 
			
		||||
</container>;
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
在上面这个例子中,当我们点击内层的容器时,只会触发 `click1`,而不会触发 `click2`,只有当我们点击外层容器时,才会触发 `click2`,这样就成功避免了内外两个按钮同时触发的场景。
 | 
			
		||||
 | 
			
		||||
### 事件对象的属性
 | 
			
		||||
 | 
			
		||||
事件包含很多属性,它们定义如下,其中 `IActionEventBase` 是 `enter` `leave` 的事件对象,`IActionEvent` 是按下、抬起、移动、点击的事件对象,`IWheelEvent` 是滚轮的事件对象。
 | 
			
		||||
 | 
			
		||||
::: code-group
 | 
			
		||||
 | 
			
		||||
```ts [IActionEventBase]
 | 
			
		||||
interface IActionEventBase {
 | 
			
		||||
    /** 当前事件是监听的哪个元素 */
 | 
			
		||||
    target: RenderItem;
 | 
			
		||||
    /** 是触摸操作还是鼠标操作 */
 | 
			
		||||
    touch: boolean;
 | 
			
		||||
    /**
 | 
			
		||||
     * 触发的按键种类,会出现在点击、按下、抬起三个事件中,而其他的如移动等该值只会是 {@link MouseType.None},
 | 
			
		||||
     * 电脑端可以有左键、中键、右键等,手机只会触发左键,每一项的值参考 {@link MouseType}
 | 
			
		||||
     */
 | 
			
		||||
    type: MouseType;
 | 
			
		||||
    /**
 | 
			
		||||
     * 当前按下了哪些按键。该值是一个数字,可以通过位运算判断是否按下了某个按键。
 | 
			
		||||
     * 例如通过 `buttons & MouseType.Left` 来判断是否按下了左键。
 | 
			
		||||
     * 注意在鼠标抬起或鼠标点击事件中,并不会包含触发的那个按键
 | 
			
		||||
     */
 | 
			
		||||
    buttons: number;
 | 
			
		||||
    /** 触发时是否按下了 alt 键 */
 | 
			
		||||
    altKey: boolean;
 | 
			
		||||
    /** 触发时是否按下了 shift 键 */
 | 
			
		||||
    shiftKey: boolean;
 | 
			
		||||
    /** 触发时是否按下了 ctrl 键 */
 | 
			
		||||
    ctrlKey: boolean;
 | 
			
		||||
    /** 触发时是否按下了 Windows(Windows) / Command(Mac) 键 */
 | 
			
		||||
    metaKey: boolean;
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
```ts [IActionEvent]
 | 
			
		||||
export interface IActionEvent extends IActionEventBase {
 | 
			
		||||
    /** 这次操作的标识符,在按下、移动、抬起阶段中保持不变 */
 | 
			
		||||
    identifier: number;
 | 
			
		||||
    /** 相对于触发元素左上角的横坐标 */
 | 
			
		||||
    offsetX: number;
 | 
			
		||||
    /** 相对于触发元素左上角的纵坐标 */
 | 
			
		||||
    offsetY: number;
 | 
			
		||||
    /** 相对于整个画布左上角的横坐标 */
 | 
			
		||||
    absoluteX: number;
 | 
			
		||||
    /** 相对于整个画布左上角的纵坐标 */
 | 
			
		||||
    absoluteY: number;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 调用后将停止事件的继续传播。
 | 
			
		||||
     * 在捕获阶段,将会阻止捕获的进一步进行,在冒泡阶段,将会阻止冒泡的进一步进行。
 | 
			
		||||
     * 如果当前元素有很多监听器,该方法并不会阻止其他监听器的执行。
 | 
			
		||||
     */
 | 
			
		||||
    stopPropagation(): void;
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
```ts [IWheelEvent]
 | 
			
		||||
export interface IWheelEvent extends IActionEvent {
 | 
			
		||||
    /** 滚轮事件的鼠标横向滚动量 */
 | 
			
		||||
    wheelX: number;
 | 
			
		||||
    /** 滚轮事件的鼠标纵向滚动量 */
 | 
			
		||||
    wheelY: number;
 | 
			
		||||
    /** 滚轮事件的鼠标垂直屏幕的滚动量 */
 | 
			
		||||
    wheelZ: number;
 | 
			
		||||
    /** 滚轮事件的滚轮类型,表示了对应值的单位 */
 | 
			
		||||
    wheelType: WheelType;
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
:::
 | 
			
		||||
 | 
			
		||||
需要特别说明的是 `identifier` 属性,这个属性在移动端的表现没有异议,但是在电脑端,我们完全可以按下鼠标左键后,再按下鼠标右键,再按下鼠标侧键,抬起鼠标右键,抬起鼠标左键,再抬起鼠标侧键,这种情况下,我们必须单独定义 `identifier` 应该指代的是哪个。它遵循如下原则:
 | 
			
		||||
 | 
			
		||||
1. 按下、抬起、点击**永远**保持为同一个 `identifier`
 | 
			
		||||
2. 移动过程中,使用最后一个按下的按键的 `identifier` 作为移动事件的 `identifier`
 | 
			
		||||
3. 如果移动过程中,最后一个按下的按键抬起,那么依然会维持**原先的** `identifer`,**不会**回退至上一个按下的按键
 | 
			
		||||
 | 
			
		||||
除此之外,滚轮事件中的 `identifier` 永远为 -1。
 | 
			
		||||
 | 
			
		||||
## 监听按键操作
 | 
			
		||||
 | 
			
		||||
### 注册按键命令
 | 
			
		||||
 | 
			
		||||
首先,我们应该注册一个按键命令,我们从 `@motajs/system-action` 中引入 `gameKey` 常量,在模块顶层注册一个按键命令:
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
import { gameKey } from '@motajs/system-action';
 | 
			
		||||
import { KeyCode } from '@motajs/client-base';
 | 
			
		||||
 | 
			
		||||
gameKey
 | 
			
		||||
    // 将后面注册的内容形成一个组,在修改快捷键时比较直观
 | 
			
		||||
    // 命名建议为 @ui_[UI 名称]
 | 
			
		||||
    .group('@ui_mybook', '示例怪物手册')
 | 
			
		||||
    .register({
 | 
			
		||||
        // 命名时,建议使用 @ui_[UI 名称]_[按键名称] 的格式
 | 
			
		||||
        id: '@ui_mybook_moveUp',
 | 
			
		||||
        // 在自定义快捷键界面显示的名称
 | 
			
		||||
        name: '上移一个怪物',
 | 
			
		||||
        // 默认按键
 | 
			
		||||
        defaults: KeyCode.ArrowUp
 | 
			
		||||
    })
 | 
			
		||||
    // 可以继续注册其他的,这里不再演示
 | 
			
		||||
    .register({});
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### 实现按键操作
 | 
			
		||||
 | 
			
		||||
然后,我们需要从 `@motajs/render` 中引入 `useKey` 函数,然后在组件顶层这么使用:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
import { useKey } from '@motajs/render';
 | 
			
		||||
 | 
			
		||||
export const MyBook = defineComponent<MyBookProps>(props => {
 | 
			
		||||
    // 第一个参数是按键实例,第二个参数是按键作用域,一般用不到
 | 
			
		||||
    const [key, scope] = useKey();
 | 
			
		||||
 | 
			
		||||
    return () => <container />;
 | 
			
		||||
});
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
最后,实现按键操作,使用 `key.realize` 方法:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
import { clamp } from 'lodash-es';
 | 
			
		||||
 | 
			
		||||
export const MyBook = defineComponent<MyBookProps>(props => {
 | 
			
		||||
    const selected = ref(0); // [!code ++]
 | 
			
		||||
    const [key, scope] = useKey();
 | 
			
		||||
 | 
			
		||||
    // 实现按键操作,让选中的怪物索引减一 // [!code ++]
 | 
			
		||||
    key.realize('@ui_mybook_moveUp', () => {
 | 
			
		||||
        // clamp 函数是 lodash 库中的函数,可以将值限定在指定范围内 // [!code ++]
 | 
			
		||||
        selected.value = clamp(0, enemys.value.length - 1, selected.value - 1); // [!code ++]
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return () => <container />;
 | 
			
		||||
});
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## 绘制选择框与动画
 | 
			
		||||
 | 
			
		||||
### 定义选择框动画
 | 
			
		||||
 | 
			
		||||
下面我们来把选择框加上,当按下方向键时,选择框会移动,当按下确定键时,会打开这个怪物的详细信息。首先,我们使用一个描边格式的 `g-rectr` 圆角矩形元素作为选择框:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
<Scroll>
 | 
			
		||||
    <g-rectr loc={[16, 16, 480 - 32, 480 - 32]} stroke strokeStyle="gold" />
 | 
			
		||||
</Scroll>
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
接下来,我们需要让它能够移动,当用户按下按键时,选择框会平滑移动到目标位置。这时候,我们可以使用动画接口 `transitioned` 来实现平滑移动。我们需要先用它定义一个动画对象:
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
// 这个函数在用户代码里面,直接引入
 | 
			
		||||
import { transitioned } from '../use';
 | 
			
		||||
// 从高级动画库中引入双曲速率曲线,该曲线视角效果相对较好
 | 
			
		||||
import { hyper } from 'mutate-animate';
 | 
			
		||||
 | 
			
		||||
// 创建一个纵坐标动画对象,初始值为 0(第一个参数),动画时长 150ms(第二个参数)
 | 
			
		||||
// 曲线为 慢-快-慢 的双曲正弦曲线(第三个参数)
 | 
			
		||||
const rectY = transitioned(0, 150, hyper('sin', 'in-out'));
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
然后,我们需要通过 `computed` 方法来动态生成圆角矩形的位置:
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
const rectLoc = computed(() => [
 | 
			
		||||
    16,
 | 
			
		||||
    // 使用 rectY.ref.value 获取到动画对象的响应式变量
 | 
			
		||||
    rectY.ref.value,
 | 
			
		||||
    480 - 32,
 | 
			
		||||
    480 - 32
 | 
			
		||||
]);
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
最后,我们把圆角矩形的 `loc` 属性设为 `computed` 值:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
<Scroll>
 | 
			
		||||
    <g-rectr loc={rectLoc.value} stroke strokeStyle="gold" />
 | 
			
		||||
</Scroll>
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### 执行动画
 | 
			
		||||
 | 
			
		||||
接下来,我们需要监听当前选中怪物,然后根据当前怪物来设置元素位置,使用 `watch` 监听 `selected` 变量:
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
watch(selected, value => {
 | 
			
		||||
    // 使用 set 方法来动画至目标值
 | 
			
		||||
    rectY.set(16 + value * 80);
 | 
			
		||||
});
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
除此之外,我们还可以添加当鼠标移动至怪物元素上时,选择框也移动至目标,我们需要监听 `onEnter` 事件:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
const onEnter = (index: number) => {
 | 
			
		||||
    // 前面已经监听过 selected 了,这里直接设置即可,不需要再调用 rectY.set
 | 
			
		||||
    // 不过调用了也不会有什么影响,动画会智能处理这种情况
 | 
			
		||||
    selected.value = index;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
<Scroll>
 | 
			
		||||
    {/* 把圆角矩形的纵深调大,防止被怪物容器遮挡 */}
 | 
			
		||||
    <g-rectr loc={rectLoc.value} stroke strokeStyle="gold" zIndex={10} />
 | 
			
		||||
    {enemys.map((v, i) => {
 | 
			
		||||
        // 元素内容不再展示。监听时,需要传入一个函数,因此需要使用匿名箭头函数包裹,
 | 
			
		||||
        // 添加 void 关键字是为了防止返回值泄漏,不过在这里并不是必要,因为 onEnter 没有返回值
 | 
			
		||||
        return <container onEnter={() => void onEnter(i)}></container>;
 | 
			
		||||
    })}
 | 
			
		||||
</Scroll>;
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### 处理重叠
 | 
			
		||||
 | 
			
		||||
如果你去尝试着使用上面这个方法来实现动画,并给每个怪物添加了一个点击事件,你会发现你可能无法触发选中怪物的点击事件,这是因为 `g-rectr` 的纵深 `zIndex` 较高,交互事件会传播至此元素,而不会传播至下层元素,于是就不会触发点击事件。样板自然也考虑到了这种情况,我们只需要给圆角矩形添加一个 `noevent` 标识,即可让交互事件不会受到此元素的影响,不过相应地,这个元素上的交互事件也将会无法触发。示例如下:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
<Scroll>
 | 
			
		||||
    <g-rectr
 | 
			
		||||
        loc={rectLoc.value}
 | 
			
		||||
        stroke
 | 
			
		||||
        strokeStyle="gold"
 | 
			
		||||
        zIndex={10}
 | 
			
		||||
        // 添加 noevent 标识,事件就不会传播至此元素
 | 
			
		||||
        noevent // [!code ++]
 | 
			
		||||
    />
 | 
			
		||||
    {enemys.map((v, i) => {
 | 
			
		||||
        return <container onEnter={() => void onEnter(i)}></container>;
 | 
			
		||||
    })}
 | 
			
		||||
</Scroll>
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## 调用 Scroll 组件接口
 | 
			
		||||
 | 
			
		||||
我们现在已经实现了按键操作,但是移动时并不能同时修改滚动条的位置,这会导致当前选中的怪物跑到画面之外,这时候我们需要自动滚动到目标位置,可以使用 `Scroll` 组件暴露出的接口来实现。我们使用 `ref` 属性来获取其接口:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
import { ScrollExpose } from './components';
 | 
			
		||||
 | 
			
		||||
const scrollExpose = ref<ScrollExpose>();
 | 
			
		||||
 | 
			
		||||
<Scroll ref={scrollExpose}></Scroll>;
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
然后,我们可以调用其 `scrollTo` 方法来滚动至目标位置:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
import { ScrollExpose } from './components';
 | 
			
		||||
 | 
			
		||||
const scrollExpose = ref<ScrollExpose>();
 | 
			
		||||
 | 
			
		||||
watch(selected, () => {
 | 
			
		||||
    // 滚动到选中怪物上下居中的位置,组件内部会自动处理滚动条边缘,因此不需要担心为负值
 | 
			
		||||
    scrollExpose.value.scrollTo(selected.value * 80 - 240);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
<Scroll ref={scrollExpose}></Scroll>;
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## 修改 UI 参数
 | 
			
		||||
 | 
			
		||||
在打开 UI 时,我们可以传入参数,默认情况下,可以传入所有的 `BaseProps`,也就是所有元素通用属性,以及自己定义的 UI 参数。`BaseProps` 内容较多,可以参考 [API 文档](../api/motajs-render-vue/RenderItem.md)。除此之外,我们还为这个自定义怪物手册添加了 `floorId` 参数,它也可以在打开 UI 时传入。如果需要打开的 UI 参数具有响应式,例如可以动态修改楼层 id,可以使用 `reactive` 方法。示例如下:
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
import { MyBookProps, MyBookUI } from './myUI';
 | 
			
		||||
 | 
			
		||||
const props = reactive<MyBookProps>({
 | 
			
		||||
    floorId: 'MT0',
 | 
			
		||||
    zIndex: 100
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
mainUIController.open(MyBookUI, props);
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
我们可以监听状态栏更新来实时更新参数:
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
import { hook } from '@user/data-base';
 | 
			
		||||
 | 
			
		||||
// 监听状态栏更新事件
 | 
			
		||||
hook.on('updateStatusBar', () => {
 | 
			
		||||
    // 状态栏更新时,修改怪物手册的楼层为当前楼层 id
 | 
			
		||||
    props.floorId = core.status.floorId,
 | 
			
		||||
});
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## 总结
 | 
			
		||||
 | 
			
		||||
通过以上的学习,你已经可以做出一个自己的怪物手册了!试着做一下吧!
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user