diff --git a/.github/workflows/page.yml b/.github/workflows/page.yml index a8e4950..e5aa123 100644 --- a/.github/workflows/page.yml +++ b/.github/workflows/page.yml @@ -27,7 +27,7 @@ jobs: run: | npm i -g pnpm@7.27.0 pnpm i - pnpm build-gh + pnpm build # 执行部署 - name: 部署 diff --git a/.gitignore b/.gitignore index 833f8af..dcbb758 100644 --- a/.gitignore +++ b/.gitignore @@ -35,4 +35,20 @@ index.cjs !public/swap/*.h5save _bundle out -dist-resource \ No newline at end of file +dist-resource +_temp +dam1.png +dam2.png +dam3.png +dam4.png +meeting.md + +*.csv +script/special.ts +script/people.ts +user.ts +.antlr +graph.svg +docs/.vitepress/cache +docs/.vitepress/dist +docs/.vitepress/apiSidebar.ts diff --git a/.madgerc b/.madgerc new file mode 100644 index 0000000..edc2560 --- /dev/null +++ b/.madgerc @@ -0,0 +1,12 @@ +{ + "fileExtensions": [ + "ts", + "tsx" + ], + "tsConfig": "./tsconfig.json", + "detectiveOptions": { + "ts": { + "skipTypeImports": true + } + } +} \ No newline at end of file diff --git a/.prettierignore b/.prettierignore index 264740c..3e797de 100644 --- a/.prettierignore +++ b/.prettierignore @@ -5,4 +5,12 @@ public/project/floors/*.js public/project/items.js public/project/floors/*.js public/project/maps.js -script/**/*.js \ No newline at end of file +public/project/icons.js +public/project/enemys.js +public/_server/**/*.js +script/**/*.js +public/editor.html +keyCodes.ts +src/core/main/setting.ts +src/core/fx/shadow.ts +dist/ \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..6ff0062 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,13 @@ +{ + "printWidth": 80, + "tabWidth": 4, + "useTabs": false, + "semi": true, + "singleQuote": true, + "quoteProps": "as-needed", + "bracketSpacing": true, + "vueIndentScriptAndStyle": false, + "arrowParens": "avoid", + "trailingComma": "none", + "endOfLine": "auto" +} \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json index a7cea0b..4c8f1f8 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,3 +1,9 @@ { - "recommendations": ["Vue.volar"] + "recommendations": [ + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "vue.volar", + "slevesque.shader", + "tobermory.es6-string-html" + ] } diff --git a/LICENSE b/LICENSE index c6e2e54..0a04128 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,165 @@ -MIT License + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 -Copyright (c) 2023 unanmed + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/components.d.ts b/components.d.ts index 46765fe..49dd79b 100644 --- a/components.d.ts +++ b/components.d.ts @@ -7,7 +7,9 @@ export {} declare module '@vue/runtime-core' { export interface GlobalComponents { + AButton: typeof import('ant-design-vue/es')['Button'] ADivider: typeof import('ant-design-vue/es')['Divider'] + AInput: typeof import('ant-design-vue/es')['Input'] AProgress: typeof import('ant-design-vue/es')['Progress'] ASelect: typeof import('ant-design-vue/es')['Select'] ASelectOption: typeof import('ant-design-vue/es')['SelectOption'] @@ -17,6 +19,7 @@ declare module '@vue/runtime-core' { BoxAnimate: typeof import('./src/components/boxAnimate.vue')['default'] Colomn: typeof import('./src/components/colomn.vue')['default'] EnemyOne: typeof import('./src/components/enemyOne.vue')['default'] + Minimap: typeof import('./src/components/minimap.vue')['default'] Scroll: typeof import('./src/components/scroll.vue')['default'] } } diff --git a/docs/.vitepress/api.ts b/docs/.vitepress/api.ts new file mode 100644 index 0000000..287f72d --- /dev/null +++ b/docs/.vitepress/api.ts @@ -0,0 +1,97 @@ +import fs from 'fs-extra'; +import path from 'path'; +import chokidar from 'chokidar'; +import { DefaultTheme } from 'vitepress'; + +const apiDir = path.resolve('./docs/api'); +const sidebarConfigPath = path.resolve('./docs/.vitepress/apiSidebar.ts'); + +const weight: Record = { + 主页: 10, + 函数: 5 +}; + +function generateSidebar(): void { + const sidebar: DefaultTheme.SidebarItem[] = [ + { text: '目录', link: '/api/' } + ]; + + // 遍历 api 目录,查找 package 目录 + const packages = fs + .readdirSync(apiDir) + .filter(pkg => fs.statSync(path.join(apiDir, pkg)).isDirectory()); + + packages.forEach(pkg => { + const pkgPath = path.join(apiDir, pkg); + const files = fs + .readdirSync(pkgPath) + .filter(file => file.endsWith('.md')); + + const items: DefaultTheme.SidebarItem[] = files.map(file => { + const filePath = `api/${pkg}/${file}`; + const fileName = path.basename(file, '.md'); + + return { + text: + fileName === 'index' + ? '主页' + : fileName === 'functions' + ? '函数' + : fileName, + link: `/${filePath.replace(/\\/g, '/')}` // 兼容 Windows 路径 + }; + }); + + items.sort((a, b) => { + const titleA = a.text ?? ''; + const titleB = b.text ?? ''; + return (weight[titleB] ?? 0) - (weight[titleA] ?? 0); + }); + + sidebar.push({ + text: pkg, + collapsed: true, + items + }); + }); + + // 生成 sidebar.ts + const sidebarContent = `import { DefaultTheme } from 'vitepress'; + +export default ${JSON.stringify( + sidebar, + null, + 4 + )} as DefaultTheme.SidebarItem[];`; + fs.writeFileSync(sidebarConfigPath, sidebarContent); + console.log('✅ Sidebar 配置已更新'); +} + +// 初次运行 +generateSidebar(); + +// 监听文件变动 +chokidar + .watch(apiDir, { ignoreInitial: true }) + .on('add', filePath => { + console.log(`📄 文件新增: ${filePath}`); + generateSidebar(); + }) + .on('unlink', filePath => { + console.log(`❌ 文件删除: ${filePath}`); + generateSidebar(); + }) + .on('addDir', dirPath => { + console.log(`📁 目录新增: ${dirPath}`); + generateSidebar(); + }) + .on('unlinkDir', dirPath => { + console.log(`📁 目录删除: ${dirPath}`); + generateSidebar(); + }) + .on('raw', (event, path, details) => { + if (event === 'rename') { + console.log(`🔄 文件或文件夹重命名: ${path}`); + generateSidebar(); + } + }); diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts new file mode 100644 index 0000000..bccf639 --- /dev/null +++ b/docs/.vitepress/config.ts @@ -0,0 +1,119 @@ +import { defineConfig } from 'vitepress'; +import { MermaidMarkdown, MermaidPlugin } from 'vitepress-plugin-mermaid'; +import api from './apiSidebar'; + +// https://vitepress.dev/reference/site-config +export default defineConfig({ + title: 'HTML5 魔塔样板 V2.B', + description: 'HTML5 魔塔样板 V2.B 帮助文档', + base: '/_docs/', + markdown: { + math: true, + config(md) { + md.use(MermaidMarkdown); + } + }, + themeConfig: { + // https://vitepress.dev/reference/default-theme-config + outline: [2, 3], + nav: [ + { text: '主页', link: '/' }, + { text: '指南', link: '/guide/diff' }, + { text: 'API', link: '/api/' }, + { text: '错误代码', link: '/logger/' } + ], + sidebar: { + '/guide/': [ + { + text: '深度指南', + items: [ + { text: '差异说明', link: '/guide/diff' }, + { text: '系统说明', link: '/guide/system' }, + { text: '代码编写', link: '/guide/coding' }, + { + text: 'UI 系统', + collapsed: false, + items: [ + { 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' } + ] + }, + { text: '音频系统', link: '/guide/audio' } + ] + } + ], + '/logger/': [ + { + text: '错误代码一览', + items: [ + { + text: '错误代码', + collapsed: false, + items: [ + { text: '1-50', link: '/logger/error/error1' } + ] + }, + { + text: '警告代码', + collapsed: false, + items: [ + { text: '1-50', link: '/logger/warn/warn1' }, + { text: '51-100', link: '/logger/warn/warn2' } + ] + } + ] + } + ], + '/api/': [ + { + text: 'API 列表', + items: api + } + ] + }, + socialLinks: [ + { icon: 'github', link: 'https://github.com/unanmed/HumanBreak' } + ], + search: { + provider: 'local', + options: { + locales: { + zh: { + translations: { + button: { + buttonText: '搜索文档', + buttonAriaLabel: '搜索文档' + }, + modal: { + noResultsText: '无法找到相关结果', + resetButtonTitle: '清除查询条件', + footer: { + selectText: '选择', + navigateText: '切换' + } + } + } + } + } + } + } + }, + locales: { + root: { + lang: 'zh', + label: '中文' + } + }, + vite: { + plugins: [MermaidPlugin()], + optimizeDeps: { + include: ['mermaid'] + }, + ssr: { + noExternal: ['mermaid'] + } + } +}); diff --git a/docs/.vitepress/theme.css b/docs/.vitepress/theme.css new file mode 100644 index 0000000..4d1d650 --- /dev/null +++ b/docs/.vitepress/theme.css @@ -0,0 +1,7 @@ +.mermaid { + max-width: 600px; /* 限制最大宽度 */ + max-height: 400px; /* 限制最大高度 */ + overflow: auto; /* 允许滚动以防止超出 */ + display: block; + margin: 0 auto; +} diff --git a/docs/api/index.md b/docs/api/index.md new file mode 100644 index 0000000..d4948fa --- /dev/null +++ b/docs/api/index.md @@ -0,0 +1,32 @@ +--- +lang: zh-CN +--- + +# API 列表 + +- [@motajs/client](./motajs-client) 渲染层代码 +- [@motajs/client-base](./motajs-client-base) 渲染层底层代码 +- [@motajs/common](./motajs-common) 渲染层和数据层通用代码 +- [@motajs/legacy-client](./motajs-legacy-client) 遗留渲染层代码 +- [@motajs/legacy-common](./motajs-legacy-common) 遗留通用代码 +- [@motajs/legacy-data](./motajs-legacy-data) 遗留数据层代码 +- [@motajs/legacy-system](./motajs-legacy-system) 遗留渲染层系统代码 +- [@motajs/legacy-ui](./motajs-legacy-ui) 遗留 UI 相关代码 +- [@motajs/render](./motajs-render) 渲染系统代码 +- [@motajs/render-core](./motajs-render-core) 渲染系统核心代码 +- [@motajs/render-elements](./motajs-render-elements) 渲染系统内置元素代码 +- [@motajs/render-style](./motajs-render-style) 渲染系统样式代码 +- [@motajs/render-vue](./motajs-render-vue) 渲染系统 vue 支持代码 +- [@motajs/system](./motajs-system) 渲染层系统代码 +- [@motajs/system-action](./motajs-system-action) 渲染层交互系统代码 +- [@motajs/system-ui](./motajs-system-ui) 渲染层 UI 系统代码 +- [@motajs/types](./motajs-types) 渲染层类型代码 +- [@user/client-modules](./user-client-modules) 用户渲染层主要代码 +- [@user/data-base](./user-data-base) 用户数据层底层代码 +- [@user/data-fallback](./user-data-fallback) 用户数据层向后兼容代码 +- [@user/data-state](./user-data-state) 用户数据层状态代码 +- [@user/data-utils](./user-data-utils) 用户数据层工具代码 +- [@user/entry-client](./user-entry-client) 用户渲染层入口 +- [@user/entry-data](./user-entry-data) 用户数据层入口 +- [@user/legacy-plugin-client](./user-legacy-plugin-client) 用户遗留渲染层代码 +- [@user/legacy-plugin-data](./user-legacy-plugin-data) 用户遗留数据层代码 diff --git a/docs/api/motajs-client-base/KeyCode.md b/docs/api/motajs-client-base/KeyCode.md new file mode 100644 index 0000000..b73bbd2 --- /dev/null +++ b/docs/api/motajs-client-base/KeyCode.md @@ -0,0 +1,3 @@ +# KeyCode + +参考 `monaco-editor` 文档 [KeyCode](https://microsoft.github.io/monaco-editor/docs.html#enums/KeyCode.html) diff --git a/docs/api/motajs-client-base/index.md b/docs/api/motajs-client-base/index.md new file mode 100644 index 0000000..163dbb2 --- /dev/null +++ b/docs/api/motajs-client-base/index.md @@ -0,0 +1,5 @@ +# @motajs/client-base + +目录: + +- [KeyCode](./KeyCode.md) diff --git a/docs/api/motajs-client/index.md b/docs/api/motajs-client/index.md new file mode 100644 index 0000000..5c5b673 --- /dev/null +++ b/docs/api/motajs-client/index.md @@ -0,0 +1,13 @@ +# @motajs/client + +`@motajs/client` 包含多个模块: + +- [`@motajs/client-base`](../motajs-client-base/) + +示例: + +```ts +import { KeyCode } from '@motajs/client'; + +const { KeyCOde } = Mota.require('@motajs/client'); +``` diff --git a/docs/api/motajs-common/Logger.md b/docs/api/motajs-common/Logger.md new file mode 100644 index 0000000..4d5bc70 --- /dev/null +++ b/docs/api/motajs-common/Logger.md @@ -0,0 +1,212 @@ +# Logger + +本文档由 `DeepSeek R1` 模型生成并微调。 + +## 属性说明 + +| 属性名 | 类型 | 默认值 | 说明 | +| --------- | ------------- | ------ | ---------------------------------------------------------------- | +| `enabled` | `boolean` | `true` | 控制日志输出是否启用。设为 `false` 可临时关闭日志输出。 | +| `level` | `LogLevel` | - | 日志级别,决定输出的最低日志等级(通过构造函数传入,不可修改)。 | +| `info` | `ILoggerInfo` | - | 包含错误和警告信息的配置对象(通过构造函数传入,不可修改)。 | + +## 方法说明 + +### `constructor` + +```ts +function constructor(level: LogLevel, info: ILoggerInfo): Logger; +``` + +#### 描述 + +构造一个 `Logger` 实例。 + +#### 参数 + +- `level`: 日志对象输出等级。 +- `info`: 日志内容。 + +### `error` + +```ts +function error(code: number, ...params: string[]): void; +``` + +#### 描述 + +记录一个错误信息。 + +#### 参数 + +- `code`: 错误代码,对应 `info.error` 中的键值。 +- `...params`: 替换错误信息中的占位符(如 $1, $2)的参数。 + +#### 行为 + +- 如果未找到对应 `code` 的错误信息,会触发 `error(16)` 表示代码未定义。 +- 根据日志级别 `level` 决定是否输出到控制台 + +### `warn` + +```ts +function warn(code: number, ...params: string[]): void; +``` + +#### 描述 + +记录一个警告信息。 + +#### 参数 + +- `code`: 警告代码,对应 `info.warn` 中的键值。 +- `...params`: 替换警告信息中的占位符的参数。 + +#### 行为 + +- 如果未找到对应 `code` 的警告信息,会触发 `error(16)`。 +- 仅在 `level <= LogLevel.WARNING` 时输出。 + +### `log` + +```ts +function log(text: string): void; +``` + +#### 描述 + +记录一条普通日志。 + +#### 参数 + +`text`: 日志文本内容。 + +#### 行为 + +- 仅在 `level <= LogLevel.LOG` 时输出到控制台。 + +### `catch` + +```ts +function catch(fn: () => T): LoggerCatchReturns; +``` + +#### 描述 + +捕获函数执行期间产生的日志信息,并抑制日志输出。 + +#### 参数 + +- `fn`: 需要执行的函数。 + +#### 返回值 + +- `ret`: 函数 `fn` 的返回值。 +- `info`: 捕获的日志信息数组。 + +#### 行为 + +- 执行期间会临时禁用日志输出,执行完成后恢复原有状态。 + +### `disable` + +```ts +function disable(): void; +``` + +#### 描述 + +禁用日志输出(设置 `enabled = false`)。 + +### `enable` + +```ts +function enable(): void; +``` + +#### 描述 + +启用日志输出(设置 `enabled = true`)。 + +## 接口说明 + +### `LoggerCatchInfo` + +#### 结构 + +```ts +interface LoggerCatchInfo { + /** 错误/警告代码(仅 error/warn 方法存在) */ + code?: number; + /** 日志等级 */ + level: LogLevel; + /** 解析后的完整信息 */ + message: string; +} +``` + +### `LoggerCatchReturns` + +#### 结构 + +```ts +interface LoggerCatchReturns { + /** 被捕获函数的返回值 */ + ret: T; + /** 捕获的日志信息列表 */ + info: LoggerCatchInfo[]; +} +``` + +## 使用示例 + +- 初始化 Logger + +```ts +import { LogLevel, Logger } from './logger'; + +const logInfo = { + error: { + 404: 'Page $1 not found.', + 500: 'Internal server error: $1' + }, + warn: { + 101: 'Deprecated API: $1' + } +}; + +const logger = new Logger(LogLevel.WARNING, logInfo); +``` + +- 记录错误 + +```ts +logger.error(404, 'home'); +// 控制台输出: [ERROR Code 404] Page home not found. +``` + +- 记录警告 + +```ts +logger.warn(101, '/old-api'); +// 控制台输出: [WARNING Code 101] Deprecated API: /old-api +``` + +- 捕获日志 + +```ts +const result = logger.catch(() => { + logger.error(500, 'database timeout'); + return { success: false }; +}); + +console.log(result.info[0].message); // "Internal server error: database timeout" +``` + +- 禁用日志 + +```ts +logger.disable(); +logger.log('This will not be printed'); // 无输出 +logger.enable(); +``` diff --git a/docs/api/motajs-common/functions.md b/docs/api/motajs-common/functions.md new file mode 100644 index 0000000..f34cf50 --- /dev/null +++ b/docs/api/motajs-common/functions.md @@ -0,0 +1,24 @@ +# @motajs/common 函数 + +## `sleep` + +```ts +function sleep(time: number): Promise; +``` + +#### 描述 + +创建一个等待指定时长的异步。 + +#### 参数 + +- `time`: 等待时长 + +#### 使用示例 + +```ts +async function myFunc() { + await sleep(1000); + // 这后面的内容会在 1 秒之后执行 +} +``` diff --git a/docs/api/motajs-common/index.md b/docs/api/motajs-common/index.md new file mode 100644 index 0000000..dd45248 --- /dev/null +++ b/docs/api/motajs-common/index.md @@ -0,0 +1,11 @@ +# @motajs/common + +目录: + +- [函数](./functions.md) + +--- + +类目录: + +- [Logger](./Logger.md) diff --git a/docs/api/motajs-legacy-client/index.md b/docs/api/motajs-legacy-client/index.md new file mode 100644 index 0000000..aa9f43e --- /dev/null +++ b/docs/api/motajs-legacy-client/index.md @@ -0,0 +1,3 @@ +# @motajs/legacy-client + +目录: diff --git a/docs/api/motajs-legacy-common/Patch.md b/docs/api/motajs-legacy-common/Patch.md new file mode 100644 index 0000000..214153d --- /dev/null +++ b/docs/api/motajs-legacy-common/Patch.md @@ -0,0 +1,120 @@ +# Patch API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +## 类描述 + +`Patch` 类用于对旧版接口的函数实现进行动态重写,支持按模块类别批量修改目标类的原型方法。需配合 `PatchClass` 枚举指定要修改的模块类型。 + +--- + +## 泛型说明 + +- `T extends PatchClass`: 表示要修改的模块类别(如 `PatchClass.Actions` 对应动作模块) + +--- + +## 属性说明 + +| 属性名 | 类型 | 描述 | +| ------------ | ---- | ---------------------------- | +| `patchClass` | `T` | 只读,当前补丁关联的模块类别 | + +--- + +## 构造方法 + +```typescript +function constructor(patchClass: T): Patch; +``` + +- **参数** + - `patchClass`: 指定要修改的模块类别(从 `PatchClass` 枚举中选择) + +**示例** + +```typescript +// 创建针对控制模块的补丁 +const patch = new Patch(PatchClass.Control); +``` + +--- + +## 方法说明 + +### `add` + +```typescript +function add( + key: K, + patch: PatchList[T][K] +): void; +``` + +为目标模块添加函数补丁。 + +- **参数** + - `key`: 要修改的函数名(需为目标模块原型存在的函数) + - `patch`: 新的函数实现 + +**示例** + +```typescript +// 重写控制模块的 setFlag 方法 +control.add('setFlag', function (this: Control, key, value) { + console.log('执行重写后的 setFlag 代码'); + if (typeof value === 'number') { + // 数字额外增加 100 点 + core.status.hero.flags[key] = value + 100; + } else { + core.status.hero.flags[key] = value; + } +}); +``` + +--- + +### `Patch.patchAll` + +```typescript +function patchAll(): void; +``` + +**静态方法**:应用所有未执行的补丁修改。一般不需要自己调用,游戏启动阶段已经包含了此方法的调用。 + +--- + +### `Patch.patch` + +```typescript +function patch(patch: Patch): void; +``` + +**静态方法**:立即应用指定补丁实例的修改。一般不需要自己调用,游戏启动阶段已经包含了此方法的调用。 + +- **参数** + - `patch`: 要应用的补丁实例 + +--- + +## 总使用示例 + +```typescript +import { Patch, PatchClass } from '@motajs/legacy-common'; + +// 新建函数,这个操作是必要的,我们不能直接在顶层使用这个接口 +export function patchMyFunctions() { + // 创建 Patch 实例,参数表示这个 Patch 示例要重写哪个文件中的函数 + // 如果需要复写两个文件,那么就需要创建两个实例 + const patch = new Patch(PatchClass.Control); + + // 使用 add 函数来重写,第一个参数会有自动补全 + // 如果要重写的函数以下划线开头,可能会有报错 + // 这时候需要去 types/declaration 中对应的文件中添加声明 + patch.add('getFlag', (name, defaultValue) => { + // 重写 getFlag,如果变量是数字,那么 +100 后返回 + const value = core.status?.hero?.flags[name] ?? defaultValue; + return typeof value === 'number' ? value + 100 : value; + }); +} +``` diff --git a/docs/api/motajs-legacy-common/index.md b/docs/api/motajs-legacy-common/index.md new file mode 100644 index 0000000..2b112e3 --- /dev/null +++ b/docs/api/motajs-legacy-common/index.md @@ -0,0 +1,3 @@ +# @motajs/legacy-common + +目录: diff --git a/docs/api/motajs-legacy-system/index.md b/docs/api/motajs-legacy-system/index.md new file mode 100644 index 0000000..914499d --- /dev/null +++ b/docs/api/motajs-legacy-system/index.md @@ -0,0 +1,3 @@ +# @motajs/legacy-system + +目录: diff --git a/docs/api/motajs-legacy-ui/index.md b/docs/api/motajs-legacy-ui/index.md new file mode 100644 index 0000000..e1375f5 --- /dev/null +++ b/docs/api/motajs-legacy-ui/index.md @@ -0,0 +1,3 @@ +# @motajs/legacy-ui + +目录: diff --git a/docs/api/motajs-render-core/Container.md b/docs/api/motajs-render-core/Container.md new file mode 100644 index 0000000..25bed56 --- /dev/null +++ b/docs/api/motajs-render-core/Container.md @@ -0,0 +1,121 @@ +# Container 类 API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +--- + +## 继承关系 + +```mermaid +graph LR + Container --> RenderItem --> EventEmitter + + click RenderItem "./RenderItem" + click EventEmitter "https://nodejs.org/api/events.html#class-eventemitter" +``` + +## 属性说明 + +| 属性名 | 类型 | 默认值 | 说明 | +| ---------------- | -------------- | ------ | ------------------------------ | +| `sortedChildren` | `RenderItem[]` | `[]` | 按 `zIndex` 排序后的子元素列表 | + +--- + +## 构造方法 + +### `constructor` + +**参数** + +- `type`: 渲染模式(`absolute` 绝对定位 / `static` 跟随摄像机) +- `cache`: 是否启用渲染缓存 +- `fall`: 是否启用变换矩阵下穿机制 + +**示例** + +```typescript +const container = new Container('static'); +``` + +--- + +## 方法说明 + +### `appendChild` + +```typescript +function appendChild(...children: RenderItem[]): void; +``` + +**描述** +添加子元素并触发重新排序。 +**示例** + +```typescript +const child = new RenderItem('static'); +container.appendChild(child); // 添加子元素 +``` + +--- + +### `removeChild` + +```typescript +function removeChild(...child: RenderItem[]): void; +``` + +**描述** +移除指定子元素并触发重新排序。 +**示例** + +```typescript +container.removeChild(child); // 移除子元素 +``` + +--- + +### `requestSort` + +```typescript +function requestSort(): void; +``` + +**描述** +标记需要重新排序子元素(在下一帧前自动执行)。 + +--- + +### `forEachChild` + +```typescript +function forEachChild(fn: (ele: RenderItem) => void): void; +``` + +**描述** +遍历元素的每一个子元素(DFS 遍历),并对每一个元素执行函数。 + +--- + +## 总使用示例 + +```typescript +// 创建基础容器 +const baseContainer = new Container('absolute'); +baseContainer.size(800, 600); + +// 添加子元素 +const sprite1 = new Sprite('static'); +sprite1.pos(100, 100).setZIndex(2); +baseContainer.appendChild(sprite1); + +const sprite2 = new Sprite('static'); +sprite2.pos(200, 200).setZIndex(1); +baseContainer.appendChild(sprite2); + +// 将容器添加到根元素 +rootElement.appendChild(baseContainer); + +// 动态修改子元素层级 +sprite1.setZIndex(0); // 自动触发重新排序 +``` diff --git a/docs/api/motajs-render-core/ContainerCustom.md b/docs/api/motajs-render-core/ContainerCustom.md new file mode 100644 index 0000000..f269f73 --- /dev/null +++ b/docs/api/motajs-render-core/ContainerCustom.md @@ -0,0 +1,71 @@ +# ContainerCustom 类 API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +--- + +## 继承关系 + +```mermaid +graph LR + ContainerCustom --> Container --> RenderItem --> EventEmitter + + click Container "./Container" + click RenderItem "./RenderItem" + click EventEmitter "https://nodejs.org/api/events.html#class-eventemitter" +``` + +--- + +## 属性说明 + +| 属性名 | 类型 | 默认值 | 说明 | +| ---------- | ------------------------- | ----------- | ---------------------- | +| `renderFn` | `CustomContainerRenderFn` | `undefined` | 自定义渲染函数(可选) | + +--- + +## 构造方法 + +继承自 `Container`,参数与父类一致。 + +--- + +## 方法说明 + +### `setRenderFn` + +```typescript +function setRenderFn(render?: CustomContainerRenderFn): void; +``` + +**描述** +设置自定义渲染函数,覆盖默认的子元素渲染逻辑。 +**参数** + +- `render`: 接收画布、子元素列表和变换矩阵的回调函数 + +**示例** + +```typescript +customContainer.setRenderFn((canvas, children, transform) => { + children.forEach(child => { + child.renderContent(canvas, transform); + }); +}); +``` + +--- + +## 总使用示例 + +```ts +// 创建自定义容器 +const customContainer = new ContainerCustom('static'); +customContainer.setRenderFn((canvas, children) => { + // 倒序渲染子元素 + children.reverse().forEach(child => { + child.renderContent(canvas, Transform.identity); + }); +}); +``` diff --git a/docs/api/motajs-render-core/Event.md b/docs/api/motajs-render-core/Event.md new file mode 100644 index 0000000..bfe4894 --- /dev/null +++ b/docs/api/motajs-render-core/Event.md @@ -0,0 +1,201 @@ +# Event 模块 API 文档 + +以下内容由 `DeepSeek R1` 模型生成并微调。 + +--- + +## 枚举说明 + +### MouseType + +| 值 | 说明 | +| --------- | ---------------- | +| `None` | 没有按键按下 | +| `Left` | 左键 | +| `Middle` | 中键(按下滚轮) | +| `Right` | 右键 | +| `Back` | 侧键后退 | +| `Forward` | 侧键前进 | + +### WheelType + +| 值 | 说明 | +| ------- | -------------------------- | +| `None` | 无单位 | +| `Pixel` | 以像素为单位 | +| `Line` | 以行为单位(约 1rem) | +| `Page` | 以页为单位(一个屏幕高度) | + +### ActionType + +| 值 | 说明 | +| ------- | -------------------------------- | +| `Click` | 点击事件(按下与抬起在同一元素) | +| `Down` | 鼠标或手指按下事件 | +| `Move` | 鼠标或手指移动事件 | +| `Up` | 鼠标或手指抬起事件 | +| `Enter` | 进入元素时触发 | +| `Leave` | 离开元素时触发 | +| `Wheel` | 滚轮事件 | + +### EventProgress + +| 值 | 说明 | +| --------- | -------- | +| `Capture` | 捕获阶段 | +| `Bubble` | 冒泡阶段 | + +--- + +## 接口说明 + +### IActionEventBase + +| 属性名 | 类型 | 说明 | +| ---------- | ------------ | ----------------------------------------------------------------- | +| `target` | `RenderItem` | 触发事件的元素 | +| `touch` | `boolean` | 是否为触摸操作(`true` 表示触摸,`false` 表示鼠标) | +| `type` | `MouseType` | 触发事件的按键类型(参考 `MouseType`) | +| `buttons` | `number` | 当前按下的按键(通过位运算判断,例如 `buttons & MouseType.Left`) | +| `altKey` | `boolean` | 是否按下 `Alt` 键 | +| `shiftKey` | `boolean` | 是否按下 `Shift` 键 | +| `ctrlKey` | `boolean` | 是否按下 `Ctrl` 键 | +| `metaKey` | `boolean` | 是否按下 `Windows/Command` 键 | + +--- + +### IActionEvent + +#### 继承关系 + +```mermaid +graph LR + IActionEvent --> IActionEventBase +``` + +| 属性名 | 类型 | 说明 | +| ------------ | -------- | -------------------------------------------- | +| `identifier` | `number` | 操作的唯一标识符(在按下、移动、抬起中一致) | +| `offsetX` | `number` | 相对于元素左上角的横坐标 | +| `offsetY` | `number` | 相对于元素左上角的纵坐标 | +| `absoluteX` | `number` | 相对于整个画布左上角的横坐标 | +| `absoluteY` | `number` | 相对于整个画布左上角的纵坐标 | + +#### 方法说明 + +##### `stopPropagation` + +```typescript +function stopPropagation(): void; +``` + +**描述** +停止事件的传播(捕获或冒泡阶段)。 + +**示例** + +```typescript +item.on('click', ev => { + ev.stopPropagation(); // 阻止事件继续传播 +}); +``` + +--- + +### IWheelEvent + +```mermaid +graph LR + IWheelEvent --> IActionEvent --> IActionEventBase +``` + +| 属性名 | 类型 | 说明 | +| ----------- | ----------- | -------------------- | +| `wheelX` | `number` | 横向滚动量 | +| `wheelY` | `number` | 纵向滚动量 | +| `wheelZ` | `number` | 垂直屏幕方向的滚动量 | +| `wheelType` | `WheelType` | 滚动量的单位类型 | + +--- + +### ERenderItemActionEvent + +描述了所有的交互事件类型。 + +| 事件名 | 参数类型 | 说明 | +| -------------- | ---------------------------- | ------------------ | +| `clickCapture` | `Readonly` | 点击事件的捕获阶段 | +| `click` | `Readonly` | 点击事件的冒泡阶段 | +| `downCapture` | `Readonly` | 按下事件的捕获阶段 | +| `down` | `Readonly` | 按下事件的冒泡阶段 | +| `moveCapture` | `Readonly` | 移动事件的捕获阶段 | +| `move` | `Readonly` | 移动事件的冒泡阶段 | +| `upCapture` | `Readonly` | 抬起事件的捕获阶段 | +| `up` | `Readonly` | 抬起事件的冒泡阶段 | +| `enter` | `Readonly` | 进入元素事件 | +| `leave` | `Readonly` | 离开元素事件 | +| `wheelCapture` | `Readonly` | 滚轮事件的捕获阶段 | +| `wheel` | `Readonly` | 滚轮事件的冒泡阶段 | + +--- + +### ActionEventMap + +| 键(ActionType) | 值类型 | 说明 | +| ---------------- | -------------- | ------------ | +| `Click` | `IActionEvent` | 点击事件 | +| `Down` | `IActionEvent` | 按下事件 | +| `Enter` | `IActionEvent` | 进入元素事件 | +| `Leave` | `IActionEvent` | 离开元素事件 | +| `Move` | `IActionEvent` | 移动事件 | +| `Up` | `IActionEvent` | 抬起事件 | +| `Wheel` | `IWheelEvent` | 滚轮事件 | + +--- + +## 总使用示例 + +::: code-group + +```typescript +// 创建渲染元素(以 Sprite 为例) +const item = new Sprite(); + +// 监听点击事件(冒泡阶段) +item.on('click', ev => { + console.log('点击坐标:', ev.offsetX, ev.offsetY); + ev.stopPropagation(); // 阻止冒泡 +}); + +// 监听滚轮事件(捕获阶段) +item.on('wheelCapture', ev => { + console.log('滚轮滚动量:', ev.wheelY, '单位:', WheelType[ev.wheelType]); +}); + +// 监听进入元素事件 +item.on('enter', ev => { + console.log('进入元素,触发按键:', MouseType[ev.type]); +}); +``` + +```tsx +// 监听点击事件(冒泡阶段) +const click = (ev: IActionEvent) => { + console.log('点击坐标:', ev.offsetX, ev.offsetY); + ev.stopPropagation(); // 阻止冒泡 +}; + +// 监听滚轮事件(捕获阶段) +const wheel = (ev: IWheelEvent) => { + console.log('滚轮滚动量:', ev.wheelY, '单位:', WheelType[ev.wheelType]); +}; + +// 监听进入元素事件 +const enter = (ev: IActionEventBase) => { + console.log('进入元素,触发按键:', MouseType[ev.type]); +}; + +; +``` + +::: diff --git a/docs/api/motajs-render-core/GL2.md b/docs/api/motajs-render-core/GL2.md new file mode 100644 index 0000000..a8964a3 --- /dev/null +++ b/docs/api/motajs-render-core/GL2.md @@ -0,0 +1,151 @@ +# GL2 类 API 文档 + +**需丰富** + +本文档由 `DeepSeek R1` 模型生成并微调。 + +## 继承关系 + +```mermaid +graph LR + GL2 --> RenderItem --> EventEmitter + + click RenderItem "./RenderItem" + click EventEmitter "https://nodejs.org/api/events.html#class-eventemitter" +``` + +--- + +## 属性说明 + +| 属性名 | 类型 | 默认值 | 说明 | +| ------------------------------- | ------------------------ | ------------------ | --------------------------------------------- | +| `support` | `boolean`(静态) | 检测 WebGL2 支持性 | 标识当前环境是否支持 WebGL2 | +| `canvas` | `HTMLCanvasElement` | - | 绑定的 WebGL2 画布元素 | +| `gl` | `WebGL2RenderingContext` | - | WebGL2 渲染上下文 | +| `UNIFORM_1f` ~ `UNIFORM_4uiv` | `UniformType` 枚举 | 对应枚举值 | WebGL uniform 类型常量(共 25 种) | +| `U_MATRIX_2x2` ~ `U_MATRIX_4x4` | `UniformMatrix` 枚举 | 对应枚举值 | 矩阵类型 uniform 常量(9 种) | +| `ATTRIB_1f` ~ `ATTRIB_I4uiv` | `AttribType` 枚举 | 对应枚举值 | 顶点属性类型常量(12 种) | +| `MAX_TEXTURE_COUNT` | `number` | `0` | 最大纹理支持数量(实际值由 WebGL 上下文决定) | + +--- + +## 构造方法 + +### `constructor` + +**参数** + +- `type`: 渲染模式(`absolute` 绝对定位 / `static` 跟随摄像机) + +**行为** + +- 初始化 WebGL2 上下文 +- 自动检测 WebGL2 支持性(通过静态属性 `support`) +- 设置默认渲染模式 + +--- + +## 方法说明 + +### `createProgram` + +```typescript +function createProgram( + Program: ProgramConstructor, + vs?: string, + fs?: string +): T; +``` + +**描述** +创建 WebGL 着色器程序 +**参数** + +- `Program`: 着色器程序类(需继承 `GL2Program`) +- `vs`: 自定义顶点着色器代码(可选) +- `fs`: 自定义片元着色器代码(可选) + **示例** + +```typescript +class MyProgram extends GL2Program {} +const program = gl2.createProgram(MyProgram); +``` + +--- + +### `useProgram` + +```typescript +function useProgram(program: GL2Program): void; +``` + +**描述** +切换当前使用的着色器程序 +**示例** + +```typescript +gl2.useProgram(shaderProgram); +``` + +--- + +### `framebuffer` + +```typescript +function framebuffer( + name: string, + texture: IShaderTexture2D, + clear?: boolean +): void; +``` + +**描述** +将渲染结果输出到帧缓冲纹理 +**参数** + +- `name`: 帧缓冲名称 +- `texture`: 目标纹理对象 +- `clear`: 是否清空画布 + +--- + +### `drawScene`(抽象方法) + +```typescript +function drawScene( + canvas: MotaOffscreenCanvas2D, + gl: WebGL2RenderingContext, + program: GL2Program, + transform: Transform +): void; +``` + +**描述** +抽象渲染方法,子类必须实现具体绘制逻辑 + +--- + +## 静态方法说明 + +### `GL2.support` + +```typescript +static readonly support: boolean; +``` + +**描述** +静态只读属性,检测 WebGL2 支持性 +**示例** + +```typescript +if (GL2.support) { + // 初始化 WebGL2 功能 +} +``` + +--- + +## 总使用示例 + +暂时没有。 diff --git a/docs/api/motajs-render-core/GL2Program.md b/docs/api/motajs-render-core/GL2Program.md new file mode 100644 index 0000000..742140d --- /dev/null +++ b/docs/api/motajs-render-core/GL2Program.md @@ -0,0 +1,188 @@ +# GL2Program 类 API 文档 + +**需丰富** + +本文档由 `DeepSeek R1` 模型生成并微调。 + +--- + +## 继承关系 + +```mermaid +graph LR + GL2Program --> EventEmitter + + click EventEmitter "https://nodejs.org/api/events.html#class-eventemitter" +``` + +--- + +## 属性说明 + +| 属性名 | 类型 | 说明 | +| -------------- | ------------------------ | ------------------------------------------ | +| `gl` | `WebGL2RenderingContext` | WebGL2 渲染上下文 | +| `element` | `GL2` | 关联的 GL2 渲染元素 | +| `program` | `WebGLProgram \| null` | WebGL 着色器程序对象 | +| `renderMode` | `RenderMode` | 当前渲染模式(默认 `RenderMode.Elements`) | +| `usingIndices` | `IShaderIndices \| null` | 当前使用的顶点索引数组 | + +--- + +## 方法说明 + +### `defineUniform` + +```typescript +function defineUniform( + uniform: string, + type: T +): IShaderUniform | null; +``` + +**描述** +定义 Uniform 变量 +**参数** + +- `uniform`: Uniform 变量名 +- `type`: Uniform 类型(如 `GL2.UNIFORM_2f`) + **返回值** +- 操作对象(可设置值)或 `null`(定义失败) + +--- + +### `defineTexture` + +```typescript +function defineTexture( + name: string, + index: number, + w?: number, + h?: number +): IShaderTexture2D | null; +``` + +**描述** +定义纹理对象 +**参数** + +- `name`: 纹理名称 +- `index`: 纹理索引(建议不超过 8) +- `w`: 纹理宽度(可选) +- `h`: 纹理高度(可选) + **示例** + +```typescript +const tex = program.defineTexture('diffuse', 0, 512, 512); +``` + +--- + +### `paramElements` + +```typescript +function paramElements( + mode: GLenum, + count: number, + type: GLenum, + offset: number +): void; +``` + +**描述** +设置元素模式渲染参数 +**参数** + +- `mode`: 渲染模式(如 `gl.TRIANGLES`) +- `count`: 元素数量 +- `type`: 数据类型(如 `gl.UNSIGNED_SHORT`) +- `offset`: 数据偏移量 + +--- + +### `requestCompile` + +```typescript +function requestCompile(force?: boolean): boolean; +``` + +**描述** +请求编译着色器 +**参数** + +- `force`: 是否强制重新编译 + **返回值** +- `true` 表示编译成功 + +--- + +### `vs` + +```typescript +function vs(vs: string): void; +``` + +**描述** +设置顶点着色器代码 +**示例** + +```typescript +program.vs(` + ${GL2_PREFIX.VERTEX} + in vec4 aPosition; + void main() { + gl_Position = aPosition; + } +`); +``` + +--- + +## 事件说明 + +| 事件名 | 触发时机 | 参数类型 | +| -------- | ---------------- | -------- | +| `load` | 程序被加载使用时 | `[]` | +| `unload` | 程序被卸载时 | `[]` | + +--- + +## 总使用示例 + +```typescript +// 创建着色器程序 +const program = gl2.createProgram(GL2Program); + +// 定义着色器 +program.vs(` + ${GL2_PREFIX.VERTEX} + uniform mat4 uProjection; + in vec4 aPosition; + void main() { + gl_Position = uProjection * aPosition; + } +`); + +program.fs(` + ${GL2_PREFIX.FRAGMENT} + out vec4 fragColor; + uniform vec3 uColor; + void main() { + fragColor = vec4(uColor, 1.0); + } +`); + +// 定义 Uniform 和纹理 +const colorUniform = program.defineUniform('uColor', UniformType.Uniform3f); +const diffuseTex = program.defineTexture('diffuse', 0, 512, 512); + +// 设置渲染参数 +program.paramElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0); + +// 编译并应用 +if (program.requestCompile()) { + gl2.useProgram(program); + colorUniform?.set(1.0, 0.5, 0.2); + diffuseTex?.set(imageElement); +} +``` diff --git a/docs/api/motajs-render-core/MotaOffscreenCanvas2D.md b/docs/api/motajs-render-core/MotaOffscreenCanvas2D.md new file mode 100644 index 0000000..09ea584 --- /dev/null +++ b/docs/api/motajs-render-core/MotaOffscreenCanvas2D.md @@ -0,0 +1,311 @@ +# MotaOffscreenCanvas2D 类 API 文档 + +以下内容由 `DeepSeek R1` 模型生成并微调。 + +--- + +## 继承关系 + +```mermaid +graph LR + MotaOffscreenCanvas2D --> EventEmitter + + click EventEmitter "https://nodejs.org/api/events.html#class-eventemitter" +``` + +--- + +## 属性说明 + +| 属性名 | 类型 | 默认值 | 说明 | +| ---------------- | -------------------------- | -------- | ------------------------------------------- | +| `canvas` | `HTMLCanvasElement` | - | 关联的 HTML 画布元素 | +| `ctx` | `CanvasRenderingContext2D` | - | 画布的 2D 渲染上下文 | +| `width` | `number` | 自动计算 | 画布的逻辑宽度(不包含缩放比例) | +| `height` | `number` | 自动计算 | 画布的逻辑高度(不包含缩放比例) | +| `autoScale` | `boolean` | `false` | 是否自动跟随 `core.domStyle.scale` 进行缩放 | +| `highResolution` | `boolean` | `true` | 是否启用高清画布(根据设备像素比例缩放) | +| `antiAliasing` | `boolean` | `true` | 是否启用抗锯齿 | +| `scale` | `number` | `1` | 当前画布的缩放比例 | +| `symbol` | `number` | `0` | 更新标识符,值变化表示画布被被动清空或调整 | +| `freezed` | `boolean`(只读) | `false` | 当前画布是否被冻结(冻结后不可修改属性) | +| `active` | `boolean`(只读) | `true` | 当前画布是否处于激活状态 | + +--- + +## 构造方法 + +### `constructor` + +```ts +function constructor( + alpha: boolean = true, + canvas?: HTMLCanvasElement +): MotaOffscreenCanvas2D; +``` + +**描述** +创建一个新的离屏画布。 +**参数** + +- `alpha`: 是否启用透明度通道(默认为 `true`)。 +- `canvas`: 可指定现有画布,未提供时自动创建新画布。 + **注意** +- 在自定义渲染元素中,建议使用 `RenderItem.requireCanvas` 而非直接调用此构造函数。 + +--- + +## 方法说明 + +### `size` + +```ts +function size(width: number, height: number): void; +``` + +**描述** +设置画布的尺寸。 +**参数** + +- `width`: 逻辑宽度(最小为 1)。 +- `height`: 逻辑高度(最小为 1)。 + **行为** +- 自动计算缩放比例(考虑 `highResolution` 和 `autoScale`)。 +- 调整画布物理尺寸和样式尺寸。 + +**示例** + +```typescript +const canvas = new MotaOffscreenCanvas2D(); +canvas.size(800, 600); // 设置画布尺寸为 800x600(逻辑尺寸) +``` + +--- + +### `withGameScale` + +```ts +function withGameScale(auto: boolean): void; +``` + +**描述** +设置画布是否跟随 `core.domStyle.scale` 自动缩放。 +**参数** + +- `auto`: 是否启用自动缩放。 + +**示例** + +```typescript +canvas.withGameScale(true); // 启用自动缩放 +``` + +--- + +### `setHD` + +```ts +function setHD(hd: boolean): void; +``` + +**描述** +设置是否为高清画布(基于设备像素比例)。 +**参数** + +- `hd`: 是否启用高清模式。 + +**示例** + +```typescript +canvas.setHD(false); // 关闭高清模式 +``` + +--- + +### `setAntiAliasing` + +```ts +function setAntiAliasing(anti: boolean): void; +``` + +**描述** +设置抗锯齿功能。 +**参数** + +- `anti`: 是否启用抗锯齿。 + +**示例** + +```typescript +canvas.setAntiAliasing(false); // 关闭抗锯齿 +``` + +--- + +### `clear` + +```ts +function clear(): void; +``` + +**描述** +清空画布内容。 +**注意** + +- 冻结状态下调用此方法会触发警告。 + +**示例** + +```typescript +canvas.clear(); // 清空画布 +``` + +--- + +### `delete` + +```ts +function delete(): void +``` + +**描述** +删除画布,释放资源并解除 DOM 绑定。 + +**示例** + +```typescript +canvas.delete(); // 删除画布 +``` + +--- + +### `freeze` + +```ts +function freeze(): void; +``` + +**描述** +冻结画布,禁止修改属性,并从全局列表中移除。 + +**示例** + +```typescript +canvas.freeze(); // 冻结画布 +``` + +--- + +### `activate` + +```ts +function activate(): void; +``` + +**描述** +激活画布,使其跟随游戏缩放调整尺寸。 + +**示例** + +```typescript +canvas.activate(); // 激活画布 +``` + +--- + +### `deactivate` + +```ts +function deactivate(): void; +``` + +**描述** +停用画布,不再自动调整尺寸,可能被垃圾回收。 + +**示例** + +```typescript +canvas.deactivate(); // 停用画布 +``` + +--- + +## 静态方法说明 + +### `MotaOffscreenCanvas2D.clone` + +```ts +function clone(canvas: MotaOffscreenCanvas2D): MotaOffscreenCanvas2D; +``` + +**描述** +复制一个画布对象,结果画布将被冻结。 +**返回值** + +- 复制的画布对象(不可修改属性,但可绘制)。 + +**示例** + +```typescript +const cloned = MotaOffscreenCanvas2D.clone(sourceCanvas); // 复制画布 +``` + +--- + +### `MotaOffscreenCanvas2D.refreshAll` + +```ts +function refreshAll(force: boolean = false): void; +``` + +**描述** +刷新所有已注册画布的尺寸(仅在窗口大小变化时自动调用)。 +**参数** + +- `force`: 是否强制刷新所有画布(默认仅刷新启用 `autoScale` 的画布)。 + +--- + +## 事件类型 + +### `resize` + +**触发时机** +当画布被动调整尺寸时触发(例如窗口大小变化或 `core.domStyle.scale` 变化)。 + +**监听示例** + +```typescript +canvas.on('resize', () => { + console.log('画布尺寸已调整'); +}); +``` + +--- + +## 使用示例 + +```typescript +// 创建画布 +const canvas = new MotaOffscreenCanvas2D(); + +// 配置属性 +canvas.size(800, 600); +canvas.withGameScale(true); +canvas.setHD(true); + +// 监听调整事件 +canvas.on('resize', () => { + console.log('画布尺寸已更新'); +}); + +// 绘制内容 +canvas.ctx.fillStyle = 'red'; +canvas.ctx.fillRect(0, 0, canvas.width, canvas.height); + +// 冻结画布 +canvas.freeze(); + +// 复制画布 +const cloned = MotaOffscreenCanvas2D.clone(canvas); +``` diff --git a/docs/api/motajs-render-core/MotaRenderer.md b/docs/api/motajs-render-core/MotaRenderer.md new file mode 100644 index 0000000..aa0940f --- /dev/null +++ b/docs/api/motajs-render-core/MotaRenderer.md @@ -0,0 +1,174 @@ +# MotaRenderer 类 API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +--- + +## 继承关系 + +```mermaid +graph LR + MotaRenderer --> Container --> RenderItem --> EventEmitter + + click Container "./Container" + click RenderItem "./RenderItem" + click EventEmitter "https://nodejs.org/api/events.html#class-eventemitter" +``` + +--- + +## 属性说明 + +| 属性名 | 类型 | 默认值 | 说明 | +| -------- | ------------------------- | ----------- | ------------------------------- | +| `isRoot` | `boolean` | `true` | 标识为渲染树根节点 | +| `target` | `MotaOffscreenCanvas2D` | - | 绑定的目标画布 | +| `idMap` | `Map` | `new Map()` | ID 到渲染元素的映射表(受保护) | + +--- + +## 构造方法 + +### `constructor` + +**参数** + +- `id`: 目标 canvas 元素的 DOM ID(默认为 `render-main`) + +**行为** + +- 自动绑定指定 ID 的 canvas 元素 +- 初始化渲染循环和事件监听 +- 设置默认锚点为中心点(0.5, 0.5) + +**示例** + +```typescript +// 创建主渲染器 +const renderer = new MotaRenderer(); + +// 创建带自定义 ID 的渲染器 +const customRenderer = new MotaRenderer('game-canvas'); +``` + +--- + +## 方法说明 + +### `getElementById` + +```typescript +function getElementById(id: string): RenderItem | null; +``` + +**描述** +通过 ID 获取渲染树中的元素。 +**示例** + +```typescript +const hero = renderer.getElementById('player'); +``` + +--- + +### `refresh` + +```typescript +function refresh(): void; +``` + +**描述** +强制刷新渲染内容(清空画布并重新渲染所有元素)。 + +--- + +### `toTagTree` + +```typescript +function toTagTree(space?: number): string; +``` + +**描述** +(调试用)将渲染树输出为 XML 格式字符串。 +**参数** + +- `space`: 缩进空格数 + **示例** + +```typescript +console.log(renderer.toTagTree()); +/* 输出示例: + + + + + +*/ +``` + +--- + +### `destroy` + +```typescript +function destroy(): void; +``` + +**描述** +销毁渲染器,释放所有资源并解除事件监听。 + +--- + +## 静态方法说明 + +### `MotaRenderer.get` + +```typescript +function get(id: string): MotaRenderer | undefined; +``` + +**描述** +通过 ID 获取已注册的渲染器实例。 +**示例** + +```typescript +const mainRenderer = MotaRenderer.get('render-main'); +``` + +--- + +## 总使用示例 + +```typescript +// 初始化渲染器 +const renderer = new MotaRenderer(); + +// 创建游戏元素 +const player = new Sprite(); +player.size(32, 32); +player.setRenderFn(canvas => { + canvas.ctx.fillStyle = 'blue'; + canvas.ctx.fillRect(0, 0, 32, 32); +}); + +// 添加交互逻辑 +player.on('click', ev => { + console.log('玩家被点击', ev.offsetX, ev.offsetY); +}); + +// 构建场景 +const scene = new Container('absolute'); +scene.appendChild(player); +renderer.appendChild(scene); + +// 动态查找元素 +setTimeout(() => { + const found = renderer.getElementById('player'); + found?.pos(100, 100); +}, 1000); + +// 销毁渲染器(退出时调用) +window.addEventListener('beforeunload', () => { + renderer.destroy(); +}); +``` diff --git a/docs/api/motajs-render-core/RenderAdapter.md b/docs/api/motajs-render-core/RenderAdapter.md new file mode 100644 index 0000000..d9146f2 --- /dev/null +++ b/docs/api/motajs-render-core/RenderAdapter.md @@ -0,0 +1,230 @@ +# RenderAdapter API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +--- + +```mermaid +graph LR + RenderAdapter --> 无继承关系 +``` + +_RenderAdapter 为独立类,无父类或子类。_ + +--- + +## 属性说明 + +| 属性名 | 类型 | 描述 | +| ------------------ | --------------------------------- | ------------------------------ | +| `items` | `Set` | 所有元素的集合 | +| `id` | `string` | 适配器的唯一标识符 | +| `adapters`(静态) | `Map>` | 全局存储所有已创建的适配器实例 | + +--- + +## 构造方法 + +### `constructor` + +```typescript +function constructor(id: string): RenderAdapter; +``` + +创建一个适配器实例并自动注册到全局 `adapters` 集合中。 +**示例:** + +```typescript +const adapter = new RenderAdapter('ui-elements'); +``` + +--- + +## 方法说明 + +### `add` + +```typescript +function add(item: T): void; +``` + +向集合中添加一个元素。 +**示例:** + +```typescript +adapter.add(document.getElementById('box')); +``` + +### `remove` + +```typescript +function remove(item: T): void; +``` + +从集合中移除一个元素。 +**示例:** + +```typescript +adapter.remove(document.getElementById('box')); +``` + +### `receiveGlobal` + +```typescript +function receiveGlobal( + id: string, + fn: (...params: any[]) => Promise +): void; +``` + +注册全局异步函数(不与具体元素绑定)。 +**示例:** + +```typescript +adapter.receiveGlobal('refresh', async () => { + await fetchData(); +}); +``` + +### `receive` + +```typescript +function receive( + id: string, + fn: (item: T, ...params: any[]) => Promise +): void; +``` + +注册元素的异步执行函数。 +**示例:** + +```typescript +adapter.receive('fadeOut', async (element: HTMLElement) => { + element.style.opacity = '0'; + await new Promise(resolve => setTimeout(resolve, 1000)); +}); +``` + +### `receiveSync` + +```typescript +function receiveSync(id: string, fn: (item: T, ...params: any[]) => any): void; +``` + +注册元素的同步执行函数。 +**示例:** + +```typescript +adapter.receiveSync('highlight', (element: HTMLElement) => { + element.style.backgroundColor = 'yellow'; +}); +``` + +### `all` + +```typescript +function all(fn: string, ...params: any[]): Promise; +``` + +对所有元素执行异步函数,返回 `Promise.all` 结果。 +**示例:** + +```typescript +await adapter.all('fadeOut'); // 所有元素淡出 +``` + +### `any` + +```typescript +function any(fn: string, ...params: any[]): Promise; +``` + +对所有元素执行异步函数,返回 `Promise.any` 结果。 +**示例:** + +```typescript +await adapter.any('loadImage'); // 任一图片加载完成即继续 +``` + +### `sync` + +```typescript +function sync(fn: string, ...params: any[]): R[]; +``` + +对所有元素执行同步函数。 +**示例:** + +```typescript +adapter.sync('highlight'); // 所有元素高亮 +``` + +### `global` + +```typescript +function global(id: string, ...params: any[]): Promise; +``` + +调用全局异步函数。 +**示例:** + +```typescript +await adapter.global('refresh'); // 触发全局刷新 +``` + +### `destroy` + +```typescript +function destroy(): void; +``` + +销毁适配器实例并从全局 `adapters` 中移除。 +**示例:** + +```typescript +adapter.destroy(); +``` + +--- + +## 静态方法说明 + +### `RenderAdapter.get` + +```typescript +function get(id: string): RenderAdapter | undefined; +``` + +通过 ID 获取已注册的适配器实例。 +**示例:** + +```typescript +const adapter = RenderAdapter.get('ui-elements'); +``` + +--- + +## 总使用示例 + +```typescript +// 创建适配器 +const animationAdapter = new RenderAdapter('animations'); + +// 注册动画函数 +animationAdapter.receive('slideLeft', async (element: HTMLElement) => { + element.style.transform = 'translateX(-100px)'; + await new Promise(resolve => setTimeout(resolve, 500)); +}); + +// 添加元素 +const box = document.getElementById('box'); +animationAdapter.add(box); + +// 执行动画 +animationAdapter.all('slideLeft').then(() => { + console.log('所有元素滑动完成'); +}); + +// 销毁适配器 +animationAdapter.destroy(); +``` diff --git a/docs/api/motajs-render-core/RenderItem.md b/docs/api/motajs-render-core/RenderItem.md new file mode 100644 index 0000000..40ca605 --- /dev/null +++ b/docs/api/motajs-render-core/RenderItem.md @@ -0,0 +1,477 @@ +# RenderItem 类 API 文档 + +**需丰富** + +本文档由 `DeepSeek R1` 模型生成并微调。 + +--- + +## 继承关系 + +```mermaid +graph LR + RenderItem --> EventEmitter + + click EventEmitter "https://nodejs.org/api/events.html#class-eventemitter" +``` + +--- + +## 接口说明 + +### IRenderUpdater + +```typescript +interface IRenderUpdater { + update(item?: RenderItem): void; +} +``` + +**描述** +定义元素更新能力的接口。`RenderItem` 通过 `update` 方法通知父元素需要重新渲染。 + +--- + +### IRenderAnchor + +```typescript +interface IRenderAnchor { + anchorX: number; + anchorY: number; + setAnchor(x: number, y: number): void; +} +``` + +**描述** +管理元素锚点的接口。锚点用于定位元素的渲染基准点(如中心点、左上角等)。 + +--- + +### IRenderConfig + +```typescript +interface IRenderConfig { + highResolution: boolean; + antiAliasing: boolean; + setHD(hd: boolean): void; + setAntiAliasing(anti: boolean): void; +} +``` + +**描述** +管理画布渲染配置的接口,控制高清模式和抗锯齿的开关。 + +--- + +### IRenderChildable + +```typescript +interface IRenderChildable { + children: Set; + appendChild(...child: RenderItem[]): void; + removeChild(...child: RenderItem[]): void; + requestSort(): void; +} +``` + +**描述** +管理子元素的接口。需在子类中实现具体逻辑(如 `Container` 元素)。 + +## 属性说明 + +| 属性名 | 类型 | 默认值 | 说明 | +| ---------------- | -------------------------- | --------------- | ---------------------------------------------------- | +| `uid` | `number` | 自动递增 | 元素的唯一标识符 | +| `id` | `string` | `''` | 元素 ID(原则不可重复) | +| `zIndex` | `number` | `0` | 元素的纵深层级(决定遮挡关系) | +| `width` | `number` | `200` | 元素的逻辑宽度 | +| `height` | `number` | `200` | 元素的逻辑高度 | +| `anchorX` | `number` | `0` | 锚点横坐标(`0` 左端,`1` 右端) | +| `anchorY` | `number` | `0` | 锚点纵坐标(`0` 上端,`1` 下端) | +| `type` | `RenderItemPosition` | `'static'` | 渲染模式(`absolute` 绝对定位,`static` 跟随摄像机) | +| `highResolution` | `boolean` | `true` | 是否启用高清画布 | +| `antiAliasing` | `boolean` | `true` | 是否启用抗锯齿 | +| `hidden` | `boolean` | `false` | 元素是否隐藏 | +| `filter` | `string` | `'none'` | 元素的滤镜效果 | +| `composite` | `GlobalCompositeOperation` | `'source-over'` | 渲染混合模式 | +| `alpha` | `number` | `1` | 元素的不透明度(`0` 透明,`1` 不透明) | +| `cursor` | `string` | `'inherit'` | 鼠标悬停时的光标样式 | +| `noEvent` | `boolean` | `false` | 是否忽略交互事件 | +| `isRoot` | `boolean` | `false` | 是否为根元素(需实现 `IRenderTreeRoot` 接口) | +| `connected` | `boolean` | 自动计算 | 元素是否已连接到根元素 | + +**特殊属性说明** + +- `transform`: 元素的变换矩阵(`Transform` 实例),修改时会自动触发 `updateTransform`。 + +--- + +## 构造方法 + +### `constructor(type: RenderItemPosition, enableCache: boolean = true, transformFallThrough: boolean = false)` + +**参数** + +- `type`: 渲染模式(`absolute` 或 `static`) +- `enableCache`: 是否启用渲染缓存(默认启用) +- `transformFallThrough`: 是否启用变换矩阵下穿机制(默认关闭) + +**示例** + +```typescript +const item = new RenderItem('absolute'); +``` + +--- + +## 方法说明 + +### `size` + +```typescript +function size(width: number, height: number): void; +``` + +**描述** +设置元素的尺寸。 +**示例** + +```typescript +item.size(300, 200); // 设置宽度 300,高度 200 +``` + +--- + +### `pos` + +```typescript +function pos(x: number, y: number): void; +``` + +**描述** +设置元素的坐标(等效于修改 `transform` 的平移量)。 +**示例** + +```typescript +item.pos(100, 50); // 设置坐标为 (100, 50) +``` + +--- + +### `append` + +```typescript +function append(parent: RenderItem): void; +``` + +**描述** +将元素添加到指定父元素下。 +**示例** + +```typescript +const parent = new RenderItem('static'); +item.append(parent); // 将 item 添加为 parent 的子元素 +``` + +--- + +### `remove` + +```typescript +function remove(): boolean; +``` + +**描述** +从父元素中移除当前元素。 +**返回值** + +- `true` 表示移除成功,`false` 表示失败。 + **示例** + +```typescript +item.remove(); // 从父元素中移除 +``` + +--- + +### `hide` + +```typescript +function hide(): void; +``` + +**描述** +隐藏元素。 +**示例** + +```typescript +item.hide(); // 隐藏元素 +``` + +--- + +### `show` + +```typescript +function show(): void; +``` + +**描述** +显示元素。 +**示例** + +```typescript +item.show(); // 显示元素 +``` + +--- + +### `delegateTicker` + +```typescript +function delegateTicker(fn: TickerFn, time?: number, end?: () => void): number; +``` + +**描述** +委托动画帧函数,持续执行指定时间。 +**返回值** + +- 委托 ID,可用于移除。 + **示例** + +```typescript +const id = item.delegateTicker(() => { + console.log('每帧执行'); +}, 1000); // 持续 1 秒 +``` + +--- + +### `destroy` + +```typescript +function destroy(): void; +``` + +**描述** +销毁元素,释放资源。 +**示例** + +```typescript +item.destroy(); // 销毁元素 +``` + +### `getAbsolutePosition` + +```typescript +function getAbsolutePosition(x?: number, y?: number): [number, number]; +``` + +**描述** +获取元素在全局坐标系中的绝对坐标。 +**示例** + +```typescript +const [absX, absY] = item.getAbsolutePosition(); // 获取元素原点绝对坐标 +``` + +--- + +### `getBoundingRect` + +```typescript +function getBoundingRect(): DOMRectReadOnly; +``` + +**描述** +获取元素的包围矩形(相对于父元素坐标系)。 +**示例** + +```typescript +const rect = item.getBoundingRect(); +console.log(rect.width, rect.height); +``` + +--- + +### `setZIndex` + +```typescript +function setZIndex(zIndex: number): void; +``` + +**描述** +设置元素的纵深层级(`zIndex` 越大越靠前)。 +**示例** + +```typescript +item.setZIndex(5); // 置顶显示 +``` + +--- + +### `requestRenderFrame` + +```typescript +function requestRenderFrame(fn: () => void): void; +``` + +**描述** +在下一帧渲染时执行函数(适用于需要在渲染流程中更新的操作)。 +**示例** + +```typescript +item.requestRenderFrame(() => { + item.pos(item.x + 1, item.y); // 每帧右移 1 单位 +}); +``` + +--- + +### `setFilter` + +```typescript +function setFilter(filter: string): void; +``` + +**描述** +设置元素的 CSS 滤镜效果(如模糊、灰度等)。 +**示例** + +```typescript +item.setFilter('blur(5px)'); // 添加模糊效果 +``` + +--- + +## 受保护方法说明 + +### `render` + +```typescript +protected abstract render(canvas: MotaOffscreenCanvas2D, transform: Transform): void; +``` + +**描述** +抽象渲染方法,子类必须实现此方法以定义具体渲染逻辑。 +**示例** + +```typescript +class CustomItem extends RenderItem { + protected render(canvas: MotaOffscreenCanvas2D) { + canvas.ctx.fillStyle = 'red'; + canvas.ctx.fillRect(0, 0, this.width, this.height); + } +} +``` + +--- + +### `isActionInElement` + +```typescript +protected isActionInElement(x: number, y: number): boolean; +``` + +**描述** +判断坐标点是否在元素范围内(可覆盖实现自定义碰撞检测)。 +**默认行为** +检测坐标是否在 `[0, width] x [0, height]` 矩形内。 + +--- + +## 静态方法说明 + +### `RenderItem.ticker` + +**类型** + +```typescript +const ticker: Ticker; +``` + +**描述** +全局动画帧管理器,用于处理所有委托的动画帧函数。 + +--- + +## 事件说明 + +事件继承自 [ERenderItemActionEvent](./Event.md#erenderitemactionevent) + +| 事件名 | 参数类型 | 说明 | +| -------------- | ------------------------- | ------------------ | +| `beforeRender` | `Transform` | 渲染前触发 | +| `afterRender` | `Transform` | 渲染后触发 | +| `destroy` | `[]` | 元素销毁时触发 | +| `transform` | `[RenderItem, Transform]` | 变换矩阵更新时触发 | + +--- + +## 总使用示例 + +::: code-group + +```typescript [基础操作] +// 创建元素(以 Sprite 为例) +const item = new Sprite('static'); + +// 设置属性 +item.size(400, 300); +item.pos(100, 50); +item.setAnchor(0.5, 0.5); // 设置中心锚点 +item.setZIndex(2); + +// 监听渲染事件 +item.on('beforeRender', transform => { + console.log('即将渲染,变换矩阵:', transform); +}); + +// 添加动画效果 +const tickerId = item.delegateTicker(time => { + item.pos(Math.sin(time / 1000) * 100, 50); +}); + +// 销毁元素 +setTimeout(() => { + item.destroy(); +}, 5000); +``` + +```typescript [创建自定义元素] +// 创建自定义可交互元素 +class Button extends RenderItem { + constructor() { + super('static'); + this.size(100, 40); + this.on('click', ev => { + console.log('按钮被点击!坐标:', ev.offsetX, ev.offsetY); + }); + } + + protected render(canvas: MotaOffscreenCanvas2D) { + // 绘制圆角矩形按钮 + const ctx = canvas.ctx; + ctx.fillStyle = '#4CAF50'; + ctx.roundRect(0, 0, this.width, this.height, 8); + ctx.fill(); + } +} + +// 使用按钮 +const button = new Button(); +button.pos(200, 150); +button.append(parentElement); + +// 添加鼠标悬停效果 +button.on('enter', () => button.setFilter('brightness(1.2)')); +button.on('leave', () => button.setFilter('none')); +``` + +::: + +# RenderItem 类 API 文档(补充部分) + +本文档由 `DeepSeek R1` 模型生成并微调。 + +--- diff --git a/docs/api/motajs-render-core/Shader.md b/docs/api/motajs-render-core/Shader.md new file mode 100644 index 0000000..b213a8b --- /dev/null +++ b/docs/api/motajs-render-core/Shader.md @@ -0,0 +1,12 @@ +# Shader 类 API 文档 + +```mermaid +graph LR + Shader --> GL2 --> RenderItem --> EventEmitter + + click GL2 "./GL2" + click RenderItem "./RenderItem" + click EventEmitter "https://nodejs.org/api/events.html#class-eventemitter" +``` + +用法同 [GL2](./GL2.md) diff --git a/docs/api/motajs-render-core/ShaderProgram.md b/docs/api/motajs-render-core/ShaderProgram.md new file mode 100644 index 0000000..30395a9 --- /dev/null +++ b/docs/api/motajs-render-core/ShaderProgram.md @@ -0,0 +1,11 @@ +# Shader 类 API 文档 + +```mermaid +graph LR + ShaderProgram --> GL2Program --> EventEmitter + + click GL2Program "./GL2Program" + click EventEmitter "https://nodejs.org/api/events.html#class-eventemitter" +``` + +用法同 [GL2Program](./GL2Program.md) diff --git a/docs/api/motajs-render-core/Sprite.md b/docs/api/motajs-render-core/Sprite.md new file mode 100644 index 0000000..40f07b1 --- /dev/null +++ b/docs/api/motajs-render-core/Sprite.md @@ -0,0 +1,121 @@ +# Sprite 类 API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +--- + +## 继承关系 + +```mermaid +graph LR + Sprite --> RenderItem --> EventEmitter + + click RenderItem "./RenderItem" + click EventEmitter "https://nodejs.org/api/events.html#class-eventemitter" +``` + +--- + +## 属性说明 + +| 属性名 | 类型 | 默认值 | 说明 | +| ---------- | ---------------- | ---------- | -------------------------------------- | +| `renderFn` | `RenderFunction` | `() => {}` | 自定义渲染函数,用于定义精灵的绘制逻辑 | + +--- + +## 构造方法 + +### `constructor` + +**参数** + +- `type`: 渲染模式(`absolute` 绝对定位 / `static` 跟随摄像机) +- `cache`: 是否启用渲染缓存(默认启用) +- `fall`: 是否启用变换矩阵下穿机制(默认关闭) + +**示例** + +```typescript +const sprite = new Sprite('static'); +``` + +--- + +## 方法说明 + +### `setRenderFn` + +```typescript +function setRenderFn(fn: RenderFunction): void; +``` + +**描述** +设置自定义渲染函数,用于定义精灵的具体绘制逻辑。 +**参数** + +- `fn`: 接收画布和变换矩阵的回调函数,格式为 `(canvas, transform) => void` + +**示例** + +```typescript +sprite.setRenderFn((canvas, transform) => { + // 绘制一个红色矩形 + canvas.ctx.fillStyle = 'red'; + canvas.ctx.fillRect(0, 0, sprite.width, sprite.height); +}); +``` + +--- + +## 总使用示例 + +```typescript +// 创建精灵实例 +const sprite = new Sprite('absolute'); +sprite.size(100, 100); // 设置尺寸 +sprite.pos(200, 150); // 设置坐标 + +// 定义渲染逻辑 +sprite.setRenderFn(canvas => { + const ctx = canvas.ctx; + // 绘制渐变圆形 + const gradient = ctx.createRadialGradient(50, 50, 0, 50, 50, 50); + gradient.addColorStop(0, 'yellow'); + gradient.addColorStop(1, 'orange'); + ctx.fillStyle = gradient; + ctx.beginPath(); + ctx.arc(50, 50, 50, 0, Math.PI * 2); + ctx.fill(); +}); + +// 添加到父容器 +container.appendChild(sprite); + +// 监听变换事件 +sprite.on('transform', (item, transform) => { + console.log('精灵变换矩阵更新:', transform); +}); +``` + +--- + +## 高级用法示例 + +```typescript +// 创建动态旋转精灵 +const rotatingSprite = new Sprite('static'); +rotatingSprite.size(80, 80); +rotatingSprite.setRenderFn((canvas, transform) => { + canvas.ctx.fillStyle = 'blue'; + canvas.ctx.fillRect(0, 0, 80, 80); +}); + +// 每帧旋转 +rotatingSprite.delegateTicker(time => { + rotatingSprite.transform.setRotate(time / 1000); +}); + +// 添加到场景 +rootContainer.appendChild(rotatingSprite); +``` diff --git a/docs/api/motajs-render-core/Transform.md b/docs/api/motajs-render-core/Transform.md new file mode 100644 index 0000000..a9ff837 --- /dev/null +++ b/docs/api/motajs-render-core/Transform.md @@ -0,0 +1,332 @@ +# Transform 类 API 文档 + +以下内容由 `DeepSeek R1` 模型生成并微调。 + +--- + +## 继承关系 + +```mermaid +graph LR + Transform --> EventEmitter + + click EventEmitter "https://nodejs.org/api/events.html#class-eventemitter" +``` + +--- + +## 属性说明 + +| 属性名 | 类型 | 默认值 | 说明 | +| -------------- | --------------------- | ----------- | ----------------------------------------------------------- | +| `mat` | `mat3` | 单位矩阵 | 存储当前变换的 3x3 矩阵 | +| `x` | `number` | `0` | 水平平移量 | +| `y` | `number` | `0` | 垂直平移量 | +| `scaleX` | `number` | `1` | 水平缩放比例 | +| `scaleY` | `number` | `1` | 垂直缩放比例 | +| `rad` | `number` | `0` | 旋转弧度值(范围:`[0, 2π)`) | +| `modified` | `boolean`(私有) | `false` | 标识变换是否被修改过 | +| `bindedObject` | `ITransformUpdatable` | `undefined` | 绑定的对象(当变换更新时自动调用其 `updateTransform` 方法) | + +--- + +## 构造方法 + +### `constructor` + +创建一个新的变换实例,初始化为单位矩阵。 + +```typescript +const transform = new Transform(); +``` + +--- + +## 方法说明 + +### `bind` + +```typescript +function bind(obj?: ITransformUpdatable): void; +``` + +**描述** +绑定一个对象,当变换更新时自动调用其 `updateTransform` 方法(若存在)。 + +**示例** + +```typescript +const obj = { updateTransform: () => console.log('Transform updated!') }; +transform.bind(obj); +``` + +--- + +### `reset` + +```typescript +function reset(): void; +``` + +**描述** +重置所有参数到初始状态(单位矩阵)。 + +**示例** + +```typescript +transform.reset(); // 重置为 x=0, y=0, scaleX=1, scaleY=1, rad=0 +``` + +--- + +### `scale` + +```typescript +function scale(x: number, y: number = x): this; +``` + +**描述** +叠加缩放变换(相对于当前状态)。 + +**参数** + +- `x`: 水平缩放比例 +- `y`: 垂直缩放比例(默认同 `x`) + +**示例** + +```typescript +transform.scale(2); // 水平和垂直均放大2倍 +transform.scale(1.5, 0.5); // 水平放大1.5倍,垂直缩小到0.5倍 +``` + +--- + +### `translate` + +```typescript +function translate(x: number, y: number): this; +``` + +**描述** +叠加平移变换(相对于当前状态)。 + +**参数** + +- `x`: 水平平移量 +- `y`: 垂直平移量 + +**示例** + +```typescript +transform.translate(100, 50); // 向右平移100单位,向下平移50单位 +``` + +--- + +### `rotate` + +```typescript +function rotate(rad: number): this; +``` + +**描述** +叠加旋转变换(相对于当前状态)。 + +**参数** + +- `rad`: 旋转弧度值 + +**示例** + +```typescript +transform.rotate(Math.PI / 2); // 顺时针旋转90度 +``` + +--- + +### `setScale` + +```typescript +function setScale(x: number, y: number = x): this; +``` + +**描述** +直接设置缩放比例(非叠加,覆盖当前状态)。 + +**示例** + +```typescript +transform.setScale(3, 2); // 设置水平缩放3倍,垂直缩放2倍 +``` + +--- + +### `setTranslate` + +```typescript +function setTranslate(x: number, y: number): this; +``` + +**描述** +直接设置平移量(非叠加,覆盖当前状态)。 + +**示例** + +```typescript +transform.setTranslate(200, 100); // 直接定位到(200, 100) +``` + +--- + +### `setRotate` + +```typescript +function setRotate(rad: number): this; +``` + +**描述** +直接设置旋转角度(非叠加,覆盖当前状态)。 + +**示例** + +```typescript +transform.setRotate(Math.PI); // 设置旋转180度 +``` + +--- + +### `transformed` + +```typescript +function transformed(x: number, y: number): vec3; +``` + +**描述** +将坐标点 `(x, y)` 应用当前变换矩阵,返回变换后的坐标。 + +**返回值** + +- `vec3`: 变换后的三维坐标(`[x, y, 1]`) + +**示例** + +```typescript +const point = transform.transformed(10, 20); // 应用变换后的坐标 +``` + +--- + +### `untransformed` + +```typescript +function untransformed(x: number, y: number): vec3; +``` + +**描述** +将坐标点 `(x, y)` 逆向应用当前变换矩阵,返回原坐标。 + +**示例** + +```typescript +const origin = transform.untransformed(50, 30); // 逆向变换后的坐标 +``` + +--- + +### `clone` + +```typescript +function clone(): Transform; +``` + +**描述** +复制当前变换实例。 + +**示例** + +```typescript +const cloned = transform.clone(); // 生成一个完全相同的副本 +``` + +--- + +## 静态方法说明 + +### `Transform.transformed` + +```typescript +function transformed(transform: Transform, x: number, y: number): vec3; +``` + +**描述** +静态方法,直接通过变换矩阵计算坐标点 `(x, y)` 的变换结果。 + +**示例** + +```typescript +const result = Transform.transformed(transform, 5, 5); +``` + +--- + +### `Transform.untransformed` + +```typescript +function untransformed(transform: Transform, x: number, y: number): vec3; +``` + +**描述** +静态方法,直接通过变换矩阵逆向计算坐标点 `(x, y)` 的原位置。 + +**示例** + +```typescript +const origin = Transform.untransformed(transform, 100, 50); +``` + +--- + +## 接口说明 + +### `ITransformUpdatable` + +```typescript +interface ITransformUpdatable { + updateTransform?(): void; +} +``` + +**描述** +可绑定对象的接口,当变换更新时触发 `updateTransform` 方法(可选)。 + +--- + +## 总使用示例 + +```typescript +// 创建变换实例 +const transform = new Transform(); + +// 应用变换 +transform + .translate(50, 30) + .scale(2) + .rotate(Math.PI / 4); + +// 绑定对象 +const obj = { + updateTransform: () => console.log('Transform updated!') +}; +transform.bind(obj); + +// 坐标转换 +const transformedPoint = transform.transformed(10, 10); +console.log('Transformed point:', transformedPoint); + +// 复制变换 +const cloned = transform.clone(); + +// 静态方法使用 +const staticResult = Transform.transformed(cloned, 5, 5); +``` diff --git a/docs/api/motajs-render-core/functions.md b/docs/api/motajs-render-core/functions.md new file mode 100644 index 0000000..a428eaf --- /dev/null +++ b/docs/api/motajs-render-core/functions.md @@ -0,0 +1,119 @@ +# 函数 API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +--- + +## 函数说明 + +### `isWebGLSupported` + +**功能** +检测浏览器是否支持 WebGL 1.0 +**返回值** + +- `true`: 支持 +- `false`: 不支持 + +**示例** + +```typescript +if (isWebGLSupported()) { + // 初始化 WebGL 1.0 功能 +} +``` + +--- + +### `isWebGL2Supported` + +**功能** +检测浏览器是否支持 WebGL 2.0 +**返回值** + +- `true`: 支持 +- `false`: 不支持 + +--- + +### `addTiming` + +**功能** +组合两个缓动函数为加法函数 +**数学表达** +`newTiming(p) = timing1(p) + timing2(p)` + +**参数** + +- `timing1`: 第一个缓动函数 +- `timing2`: 第二个缓动函数 + +**示例** + +```typescript +const linear = (p: number) => p; +const bounce = (p: number) => p * p; +const combined = addTiming(linear, bounce); // p + p² +``` + +--- + +### `multiplyTiming` + +**功能** +组合两个缓动函数为乘法函数 +**数学表达** +`newTiming(p) = timing1(p) * timing2(p)` + +--- + +### `isSetEqual` + +**功能** +判断两个集合是否相等(元素完全相同) +**实现逻辑** + +1. 直接引用相同 → `true` +2. 大小不同 → `false` +3. 检查 set1 是否是 set2 的子集 + +--- + +### `transformCanvas` + +**功能** +将变换矩阵应用到画布上下文 +**实现逻辑** + +```typescript +const mat = transform.mat; // 获取 3x3 矩阵 +const [a, b, , c, d, , e, f] = mat; // 分解为 2D 变换参数 +ctx.transform(a, b, c, d, e, f); // 应用变换 +``` + +**参数** + +- `canvas`: 目标画布对象 +- `transform`: 变换矩阵 + +**示例** + +```typescript +const transform = new Transform(); +transform.translate(100, 50); +transformCanvas(myCanvas, transform); // 应用平移变换 +``` + +--- + +## 工具函数关系图 + +```mermaid +graph LR + checkSupport --> isWebGLSupported + checkSupport --> isWebGL2Supported + addTiming --> 组合缓动函数 + multiplyTiming --> 组合缓动函数 + isSetEqual --> 集合操作 + transformCanvas --> 矩阵变换 +``` diff --git a/docs/api/motajs-render-core/index.md b/docs/api/motajs-render-core/index.md new file mode 100644 index 0000000..9ddbb44 --- /dev/null +++ b/docs/api/motajs-render-core/index.md @@ -0,0 +1 @@ +# @motajs/render-core diff --git a/docs/api/motajs-render-elements/BlockCacher.md b/docs/api/motajs-render-elements/BlockCacher.md new file mode 100644 index 0000000..1a05397 --- /dev/null +++ b/docs/api/motajs-render-elements/BlockCacher.md @@ -0,0 +1,369 @@ +# BlockCacher API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +--- + +```mermaid +graph LR + BlockCacher --> EventEmitter + + click EventEmitter "https://nodejs.org/api/events.html#class-eventemitter" +``` + +_继承自 `EventEmitter`,支持事件监听。_ + +--- + +## 属性说明 + +| 属性名 | 类型 | 描述 | +| ------------ | ---------------- | ------------------------------------------------------------------- | +| `width` | `number` | 区域总宽度(元素单位) | +| `height` | `number` | 区域总高度(元素单位) | +| `blockSize` | `number` | 单个分块的大小(元素单位) | +| `blockData` | `BlockData` | 分块计算结果(包含分块数量、最后一个块的尺寸等信息) | +| `cacheDepth` | `number` | 缓存深度(每个分块可存储多个缓存层) | +| `cache` | `Map` | 缓存存储结构,键为精确索引(`(x + y * blockWidth) * depth + deep`) | + +--- + +## 构造方法 + +### `constructor` + +```typescript +function constructor( + width: number, + height: number, + size: number, + depth?: number +): BlockCacher; +``` + +创建分块缓存管理器并自动计算初始分块。 +**示例:** + +```typescript +const cacher = new BlockCacher(800, 600, 64); // 800x600区域,64为分块大小 +``` + +--- + +## 方法说明 + +### `size` + +```typescript +function size(width: number, height: number): void; +``` + +重置区域尺寸并重新分块(触发 `split` 事件)。 +**示例:** + +```typescript +cacher.size(1024, 768); // 重置为1024x768区域 +``` + +### `setBlockSize` + +```typescript +function setBlockSize(size: number): void; +``` + +修改分块尺寸并重新分块(触发 `split` 事件)。 +**示例:** + +```typescript +cacher.setBlockSize(128); // 分块大小改为128 +``` + +### `setCacheDepth` + +```typescript +function setCacheDepth(depth: number): void; +``` + +调整缓存深度(最大 31),自动迁移旧缓存。 +**示例:** + +```typescript +cacher.setCacheDepth(3); // 每个分块支持3层缓存 +``` + +### `split` + +```typescript +function split(): void; +``` + +重新计算分块数据并触发 `'split'` 事件。 +**示例:** + +```typescript +cacher.split(); // 手动触发分块计算(一般无需调用) +``` + +### `clearCache` + +```typescript +function clearCache(index: number, deep: number): void; +``` + +清除指定分块索引的缓存(按二进制掩码清除深度)。 +**示例:** + +```typescript +cacher.clearCache(5, 0b101); // 清除分块5的第0层和第2层缓存 +``` + +### `clearCacheByIndex` + +```typescript +function clearCacheByIndex(index: number): void; +``` + +直接按精确索引清除单个缓存。 +**示例:** + +```typescript +cacher.clearCacheByIndex(42); // 清除精确索引42对应的缓存 +``` + +### `clearAllCache` + +```typescript +function clearAllCache(): void; +``` + +清空所有缓存并销毁关联资源。 +**示例:** + +```typescript +cacher.clearAllCache(); // 完全重置缓存 +``` + +### `getIndex` + +```typescript +function getIndex(x: number, y: number): number; +``` + +根据分块坐标获取分块索引(分块坐标 -> 分块索引)。 +**示例:** + +```typescript +const index = cacher.getIndex(2, 3); // 获取(2,3)分块的索引 +``` + +### `getIndexByLoc` + +```typescript +function getIndexByLoc(x: number, y: number): number; +``` + +根据元素坐标获取所属分块索引(元素坐标 -> 分块索引)。 +**示例:** + +```typescript +const index = cacher.getIndexByLoc(150, 200); // 元素坐标(150,200)所在分块索引 +``` + +### `getBlockXYByIndex` + +```typescript +function getBlockXYByIndex(index: number): [number, number]; +``` + +根据分块索引获取分块坐标(分块索引 -> 分块坐标)。 +**示例:** + +```typescript +const [x, y] = cacher.getBlockXYByIndex(5); // 分块5的坐标 +``` + +### `getBlockXY` + +```typescript +function getBlockXY(x: number, y: number): [number, number]; +``` + +获取一个元素位置所在的分块位置(即使它不在任何分块内)(元素索引 -> 分块坐标)。 +**示例:** + +```typescript +const [x, y] = cacher.getBlockXY(11, 24); // 指定位置所在分块位置 +``` + +### `getPreciseIndex` + +```typescript +function getPreciseIndex(x: number, y: number, deep: number): number; +``` + +根据分块坐标与 `deep` 获取一个分块的精确索引(分块坐标 -> 分块索引)。 +**示例:** + +```typescript +const index = cacher.getPreciseIndex(2, 1, 3); // 指定分块的索引 +``` + +### `getPreciseIndexByLoc` + +```typescript +function getPreciseIndexByLoc(x: number, y: number, deep: number): number; +``` + +根据元素坐标及 `deep` 获取元素所在块的精确索引(元素坐标 -> 分块索引)。 +**示例:** + +```typescript +const index = cacher.getPreciseIndexByLoc(22, 11, 3); // 指定元素所在分块的索引 +``` + +### `updateElementArea` + +```typescript +function updateElementArea( + x: number, + y: number, + width: number, + height: number, + deep: number = 2 ** 31 - 1 +): Set; +``` + +根据元素区域清除相关分块缓存(返回受影响的分块索引集合)(元素坐标->分块清空)。 +**示例:** + +```typescript +const blocks = cacher.updateElementArea(100, 100, 200, 200); // 清除200x200区域内的缓存 +``` + +### `updateArea` + +```typescript +function updateArea( + x: number, + y: number, + width: number, + height: number, + deep: number = 2 ** 31 - 1 +): Set; +``` + +更新指定分块区域内的缓存(注意坐标是分块坐标,而非元素坐标)(分块坐标->分块清空)。 +**示例:** + +```typescript +const blocks = cacher.updateArea(1, 1, 1, 1); // 清除指定分块区域内的缓存 +``` + +### `getIndexOf` + +```typescript +function getIndexOf( + x: number, + y: number, + width: number, + height: number +): Set; +``` + +传入分块坐标与范围,获取该区域内包含的所有分块索引(分块坐标->分块索引集合)。 +**示例:** + +```typescript +const blocks = cacher.getIndexOf(1, 1, 1, 1); // 清除指定分块区域内的缓存 +``` + +### `getIndexOfElement` + +```typescript +function getIndexOfElement( + x: number, + y: number, + width: number, + height: number +): Set; +``` + +传入元素坐标与范围,获取该区域内包含的所有分块索引(元素坐标->分块索引集合)。 +**示例:** + +```typescript +const blocks = cacher.getIndexOfElement(3, 5, 12, 23); // 清除指定元素区域内的缓存 +``` + +### `getRectOfIndex` + +```typescript +function getRectOfIndex(block: number): [number, number, number, number]; +``` + +获取分块索引对应的元素坐标范围(分块索引 -> 元素矩形坐标)。 +**示例:** + +```typescript +const [x1, y1, x2, y2] = cacher.getRectOfIndex(5); // 分块5的坐标范围 +``` + +### `getRectOfBlockXY` + +```typescript +function getRectOfBlockXY( + x: number, + y: number +): [number, number, number, number]; +``` + +根据分块坐标,获取这个分块所在区域的元素矩形范围(左上角横纵坐标及右下角横纵坐标)(分块坐标 -> 元素矩形坐标)。 +**示例:** + +```typescript +const [x1, y1, x2, y2] = cacher.getRectOfIndex(5); // 分块5的坐标范围 +``` + +### `destroy` + +```typescript +function destroy(): void; +``` + +摧毁这个块缓存。 + +--- + +## 事件说明 + +| 事件名 | 参数 | 描述 | +| ------- | ---- | ------------------ | +| `split` | 无 | 分块参数变更时触发 | + +--- + +## 总使用示例 + +```typescript +// 创建缓存管理器 +const cacheManager = new BlockCacher(1024, 768, 64); + +// 监听分块变化 +cacheManager.on('split', () => { + console.log('分块已重新计算'); +}); + +// 添加测试缓存项 +const blockIndex = cacheManager.getIndex(2, 3); +const preciseIndex = cacheManager.getPreciseIndex(2, 3, 0); +cacheManager.cache.set( + preciseIndex, + new CanvasCacheItem(new MotaOffscreenCanvas2D(), 1) +); + +// 清除特定区域缓存 +cacheManager.updateElementArea(150, 150, 100, 100); + +// 销毁管理器 +cacheManager.destroy(); +``` diff --git a/docs/api/motajs-render-elements/Camera.md b/docs/api/motajs-render-elements/Camera.md new file mode 100644 index 0000000..758d302 --- /dev/null +++ b/docs/api/motajs-render-elements/Camera.md @@ -0,0 +1,386 @@ +# Camera API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +--- + +```mermaid +graph LR + Camera --> EventEmitter + + click EventEmitter "https://nodejs.org/api/events.html#class-eventemitter" +``` + +_继承自 `EventEmitter`,支持事件监听。_ + +--- + +## 属性说明 + +| 属性名 | 类型 | 描述 | +| --------------------- | ------------------- | ------------------------------------------------ | +| `readonly binded` | `RenderItem` | 当前绑定的渲染元素 | +| `transform` | `Transform` | 目标变换矩阵,默认与 `binded.transform` 共享引用 | +| `protected operation` | `CameraOperation[]` | 当前应用的变换操作列表(平移/旋转/缩放) | + +--- + +## 构造方法 + +### `Camera.for` + +```typescript + function for(item: RenderItem): Camera +``` + +获取或创建与渲染元素关联的摄像机实例。 +**示例:** + +```typescript +const item = new RenderItem(); +const camera = Camera.for(item); // 获取或创建摄像机 +``` + +### `constructor` + +```typescript +function constructor(item: RenderItem): Camera; +``` + +直接创建摄像机实例(不会自动注册到全局映射)。 +**注意:** 推荐优先使用 `Camera.for()` 方法。 + +--- + +## 方法说明 + +### `disable` + +```typescript +function disable(): void; +``` + +禁用摄像机变换效果。 +**示例:** + +```typescript +camera.disable(); // 暂停所有摄像机变换 +``` + +### `enable` + +```typescript +function enable(): void; +``` + +启用摄像机变换效果。 + +### `requestUpdate` + +```typescript +function requestUpdate(): void; +``` + +请求在下一帧强制更新变换矩阵。 + +### `removeOperation` + +```typescript +function removeOperation(operation: CameraOperation): void; +``` + +移除一个变换操作。 +**参数说明** + +- `operation`: 要移除的操作 + +**示例** + +```ts +const operation = camera.addTranslate(); +camera.removeOperation(operation); +``` + +### `clearOperation` + +```ts +function clearOperation(): void; +``` + +清空变换操作列表。 + +### `addTranslate` + +```typescript +function addTranslate(): ICameraTranslate; +``` + +添加平移操作并返回操作对象。 +**示例:** + +```typescript +const translateOp = camera.addTranslate(); +translateOp.x = 100; // 设置横向偏移 +camera.requestUpdate(); +``` + +### `addRotate` + +```typescript +function addRotate(): ICameraRotate; +``` + +添加旋转操作并返回操作对象。 +**示例:** + +```typescript +const rotateOp = camera.addRotate(); +rotateOp.angle = Math.PI / 2; // 设置90度旋转 +camera.requestUpdate(); +``` + +### `addScale` + +```typescript +function addScale(): ICameraScale; +``` + +添加缩放操作并返回操作对象。 +**示例:** + +```typescript +const scaleOp = camera.addScale(); +scaleOp.x = 2; // 横向放大2倍 +camera.requestUpdate(); +``` + +### `applyAnimation` + +```ts +function applyAnimation(time: number, update: () => void): void; +``` + +施加动画。 + +**参数说明** + +- `time`: 动画时长。 +- `update`: 每帧执行的更新函数。 + +### `applyTranslateAnimation` + +```typescript +function applyTranslateAnimation( + operation: ICameraTranslate, + animate: Animation, + time: number +): void; +``` + +为平移操作绑定动画。 +**参数说明:** + +- `animate`: 预定义的动画实例 +- `time`: 动画持续时间(毫秒) + +### `applyRotateAnimation` + +```typescript +function applyRotateAnimation( + operation: ICameraRotate, + animate: Animation, + time: number +): void; +``` + +为旋转操作绑定动画。 + +### `applyScaleAnimation` + +```typescript +function applyScaleAnimation( + operation: ICameraScale, + animate: Animation, + time: number +): void; +``` + +为缩放操作绑定动画。 + +### `applyTranslateTransition` + +```typescript +function applyTranslateTransition( + operation: ICameraTranslate, + animate: Transition, + time: number +): void; +``` + +为平移操作绑定渐变。 +**参数说明:** + +- `animate`: 预定义的渐变实例 +- `time`: 渐变持续时间(毫秒) + +### `applyRotateTransition` + +```typescript +function applyRotateTransition( + operation: ICameraRotate, + animate: Transition, + time: number +): void; +``` + +为旋转操作绑定渐变。 + +### `applyScaleTransition` + +```typescript +function applyScaleTransition( + operation: ICameraScale, + animate: Transition, + time: number +): void; +``` + +为缩放操作绑定渐变。 + +### `stopAllAnimates` + +```ts +function stopAllAnimates(): void; +``` + +停止所有动画。 + +### `destroy` + +```typescript +function destroy(): void; +``` + +销毁摄像机并释放所有资源。 +**示例:** + +```typescript +camera.destroy(); // 解除绑定并清理动画 +``` + +--- + +## 事件说明 + +| 事件名 | 参数 | 描述 | +| --------- | ---- | ------------------ | +| `destroy` | 无 | 摄像机被销毁时触发 | + +--- + +## 总使用示例 + +::: code-group + +```typescript [动画] +import { Animation, linear } from 'mutate-animate'; + +// 获取摄像机实例 +const item = new Sprite(); +const camera = Camera.for(item); + +// 添加平移和缩放操作 +const translate = camera.addTranslate(); +const scale = camera.addScale(); + +// 创建动画实例 +const anim = new Animation() + .mode(linear()) + .time(1000) + .move(100, 100) + .time(800) + .scale(1.5); + +// 绑定动画 +camera.applyTranslateAnimation(translate, anim, 1000); +camera.applyScaleAnimation(scale, anim, 800); + +// 启用摄像机 +camera.enable(); + +// 销毁(当不再需要时) +setTimeout(() => camera.destroy(), 2000); +``` + +```typescript [渐变] +import { Transition, hyper } from 'mutate-animate'; +// 获取摄像机实例 +const item = new Sprite(); +const camera = Camera.for(item); + +// 添加平移和缩放操作 +const translate = camera.addTranslate(); +const scale = camera.addScale(); + +// 创建渐变实例,使用双曲正弦速率曲线 +const tran = new Transition().mode(hyper('sin', 'out')).time(1000); + +// 初始化参数,这一步不会执行渐变 +tran.value.x = 0; +tran.value.y = 0; +tran.value.size = 0; + +// 对参数执行渐变,直接设置即可 +tran.value.x = 100; +tran.value.y = 200; +tran.time(800); // 设置渐变时长为 800 毫秒 +tran.value.size = 1.5; + +// 绑定动画 +camera.applyTranslateTransition(translate, tran, 1000); +camera.applyScaleTransition(scale, tran, 800); + +// 启用摄像机 +camera.enable(); + +// 销毁(当不再需要时) +setTimeout(() => camera.destroy(), 2000); +``` + +::: + +--- + +## 接口说明 + +### `ICameraTranslate` + +```typescript +interface ICameraTranslate { + readonly type: 'translate'; + readonly from: RenderItem; + x: number; // 横向偏移量 + y: number; // 纵向偏移量 +} +``` + +### `ICameraRotate` + +```typescript +interface ICameraRotate { + readonly type: 'rotate'; + readonly from: RenderItem; + angle: number; // 旋转弧度值 +} +``` + +### `ICameraScale` + +```typescript +interface ICameraScale { + readonly type: 'scale'; + readonly from: RenderItem; + x: number; // 横向缩放比 + y: number; // 纵向缩放比 +} +``` diff --git a/docs/api/motajs-render-elements/CameraAnimation.md b/docs/api/motajs-render-elements/CameraAnimation.md new file mode 100644 index 0000000..55434de --- /dev/null +++ b/docs/api/motajs-render-elements/CameraAnimation.md @@ -0,0 +1,201 @@ +# CameraAnimation API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +--- + +```mermaid +graph LR + CameraAnimation --> EventEmitter + + click EventEmitter "https://nodejs.org/api/events.html#class-eventemitter" +``` + +_继承自 `EventEmitter`,支持事件监听。_ + +--- + +## 属性说明 + +| 属性名 | 类型 | 描述 | +| -------- | -------- | ---------------- | +| `camera` | `Camera` | 关联的摄像机实例 | + +--- + +## 构造方法 + +### `constructor` + +```typescript +function constructor(camera: Camera): CameraAnimation; +``` + +创建摄像机动画管理器,需绑定到特定 `Camera` 实例。 +**示例:** + +```typescript +const camera = Camera.for(renderItem); +const animation = new CameraAnimation(camera); +``` + +--- + +## 方法说明 + +### `translate` + +```typescript +function translate( + operation: ICameraTranslate, + x: number, + y: number, + time: number, + start: number, + timing: TimingFn +): void; +``` + +为平移操作添加动画。 +**参数说明:** + +- `x`, `y`: 目标偏移量(格子坐标,自动乘以 `32`,之后可能改动) +- `time`: 动画持续时间(毫秒) +- `start`: 动画开始时间(相对于总动画开始的延迟) +- `timing`: 缓动函数(输入时间完成度,输出动画完成度) + +### `rotate` + +```typescript +function rotate( + operation: ICameraRotate, + angle: number, + time: number, + start: number, + timing: TimingFn +): void; +``` + +为旋转操作添加动画。 +**参数说明:** + +- `angle`: 目标旋转弧度(如 `Math.PI` 表示 `180` 度) +- 其余参考[`rotate`](#rotate) + +### `scale` + +```typescript +function scale( + operation: ICameraScale, + scale: number, + time: number, + start: number, + timing: TimingFn +): void; +``` + +为缩放操作添加动画。 +**参数说明:** + +- `scale`: 目标缩放倍率(如 `1.5` 表示放大 `1.5` 倍) +- 其余参考[`rotate`](#rotate) + +### `start` + +```typescript +function start(): void; +``` + +启动所有已添加的动画,按时间顺序执行。 +**注意:** 调用后动画将按 `start` 参数定义的顺序触发。 + +### `destroy` + +```typescript +function destroy(): void; +``` + +销毁动画管理器并释放所有资源(停止未完成的动画)。 + +--- + +## 事件说明 + +| 事件名 | 参数 | 描述 | +| --------- | -------------------------------------------------------------------------------------------------------- | ---------------------------- | +| `animate` | `operation: CameraOperation` `execution: CameraAnimationExecution` `item: CameraAnimationData` | 当某个动画片段开始执行时触发 | + +--- + +## 总使用示例 + +```typescript +import { hyper, trigo } from 'mutate-animate'; + +// 创建渲染元素和摄像机 +const renderItem = new Sprite(); +const camera = Camera.for(renderItem); + +// 添加平移和旋转操作 +const translateOp = camera.addTranslate(); +const rotateOp = camera.addRotate(); + +// 创建动画管理器 +const animation = new CameraAnimation(camera); + +// 添加平移动画:1秒后开始,持续2秒,横向移动3格(3*32像素) +animation.translate( + translateOp, + 3, + 0, // x=3, y=0(自动乘32) + 2000, // 动画时长2秒 + 1000, // 延迟1秒开始 + hyper('sin', 'out') // 双曲正弦函数 +); + +// 添加旋转动画:立即开始,持续1.5秒,旋转180度 +animation.rotate( + rotateOp, + Math.PI, // 目标角度(弧度) + 1500, // 动画时长1.5秒 + 0, // 无延迟 + trigo('sin', 'out') // 正弦函数 +); + +// 启动动画 +animation.start(); + +// 监听动画事件 +animation.on('animate', (operation, execution, item) => { + console.log('动画片段开始:', item.type); +}); + +// 销毁(动画结束后) +setTimeout(() => { + animation.destroy(); + camera.destroy(); +}, 5000); +``` + +--- + +## 接口说明 + +### `CameraAnimationExecution` + +```typescript +interface { + data: CameraAnimationData[]; // 动画片段列表 + animation: Animation; // 关联的动画实例 +} +``` + +### `CameraAnimationData` + +```typescript +type CameraAnimationData = + | TranslateAnimation + | TranslateAsAnimation + | RotateAnimation + | ScaleAnimation; +``` diff --git a/docs/api/motajs-render-elements/index.md b/docs/api/motajs-render-elements/index.md new file mode 100644 index 0000000..899a74d --- /dev/null +++ b/docs/api/motajs-render-elements/index.md @@ -0,0 +1,3 @@ +# @motajs/render-elements + +目录: diff --git a/docs/api/motajs-render-style/Font.md b/docs/api/motajs-render-style/Font.md new file mode 100644 index 0000000..f5908b8 --- /dev/null +++ b/docs/api/motajs-render-style/Font.md @@ -0,0 +1,186 @@ +# Font API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +--- + +```mermaid +graph LR + Font --> IFontConfig +``` + +_实现 `IFontConfig` 接口,表示字体配置。_ + +--- + +## 接口说明 + +### `IFontConfig` + +```typescript +interface IFontConfig { + family: string; // 字体名称(如 "Arial") + size: number; // 字号数值 + sizeUnit: string; // 字号单位(推荐 "px") + weight: number; // 字重(0-1000) + italic: boolean; // 是否斜体 +} +``` + +### `FontWeight` 枚举 + +```typescript +enum FontWeight { + Light = 300, // 细体 + Normal = 400, // 常规 + Bold = 700 // 粗体 +} +``` + +--- + +## 属性说明 + +| 属性名 | 类型 | 描述 | +| ---------- | --------- | ------------------------- | +| `family` | `string` | 字体家族名称(只读) | +| `size` | `number` | 字号数值(只读) | +| `sizeUnit` | `string` | 字号单位(如 "px",只读) | +| `weight` | `number` | 字重数值(只读) | +| `italic` | `boolean` | 是否斜体(只读) | + +--- + +## 构造方法 + +### `constructor` + +```typescript +function constructor( + family?: string, + size?: number, + sizeUnit?: string, + weight?: number, + italic?: boolean +): Font; +``` + +创建字体实例,参数默认使用静态默认值。 +**示例:** + +```typescript +const font = new Font('Arial', 14, 'px', FontWeight.Bold, true); +``` + +--- + +## 方法说明 + +### `addFallback` + +```typescript +function addFallback(...fallback: Font[]): void; +``` + +添加后备字体(当主字体不可用时使用)。 +**注意:** 检测到递归添加时会触发 `logger.warn(62)` 警告。 +**示例:** + +```typescript +const mainFont = new Font('CustomFont'); +const fallback1 = new Font('Arial'); +const fallback2 = new Font('Helvetica'); +mainFont.addFallback(fallback1, fallback2); // 添加两个后备 +``` + +### `string` + +```typescript +function string(): string; +``` + +生成 CSS 字体字符串,包含后备字体。 +**示例:** + +```typescript +console.log(font.string()); // 示例输出:"italic 700 14px CustomFont, 400 16px Arial" +``` + +--- + +## 静态方法说明 + +### `Font.parse` + +```typescript +function parse(str: string): Font; +``` + +解析 CSS 字体字符串(支持多字体声明)。 +**示例:** + +```typescript +const parsed = Font.parse('italic 16px "Fira Sans", Arial'); +// 主字体:Fira Sans,后备:Arial +``` + +### `Font.setDefaults` + +```typescript +function setDefaults(font: Font): void; +``` + +全局设置默认字体参数。 +**示例:** + +```typescript +Font.setDefaults(new Font('Segoe UI', 14, 'px', 400, false)); +``` + +### `Font.clone` + +```typescript +function clone(font: Font, options: Partial): Font; +``` + +克隆字体并修改指定属性。 +**示例:** + +```typescript +const cloned = Font.clone(baseFont, { + size: 18, + weight: FontWeight.Bold +}); +``` + +--- + +## 总使用示例 + +```typescript +// 创建主字体 +const mainFont = new Font('CustomFont', 16, 'px', FontWeight.Normal); + +// 添加后备字体(注意避免循环引用) +const fallbackA = new Font('Arial'); +const fallbackB = new Font('Helvetica'); +fallbackA.addFallback(fallbackB); +// 错误示例(触发警告62): +// fallbackB.addFallback(fallbackA); + +mainFont.addFallback(fallbackA); + +// 生成 CSS 字符串 +console.log(mainFont.string()); +// 输出: "400 16px CustomFont, 400 16px Arial, 400 16px Helvetica" + +// 解析 CSS 字符串 +const parsed = Font.parse('italic 700 24px Fantasy, "Comic Sans"'); +parsed.addFallback(new Font('Verdana')); + +// 克隆并修改 +const boldFont = Font.clone(parsed, { + weight: FontWeight.Bold, + italic: false +}); +``` diff --git a/docs/api/motajs-render-style/index.md b/docs/api/motajs-render-style/index.md new file mode 100644 index 0000000..866d002 --- /dev/null +++ b/docs/api/motajs-render-style/index.md @@ -0,0 +1,3 @@ +# @motajs/render-style + +目录: diff --git a/docs/api/motajs-render-vue/BaseProps.md b/docs/api/motajs-render-vue/BaseProps.md new file mode 100644 index 0000000..ea70d55 --- /dev/null +++ b/docs/api/motajs-render-vue/BaseProps.md @@ -0,0 +1,134 @@ +# BaseProps 接口文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +--- + +## 接口定义 + +```typescript +interface BaseProps { + // 基础定位 + x?: number; // 横坐标(单位:像素) + y?: number; // 纵坐标(单位:像素) + anchorX?: number; // 横向锚点比例(0~1,默认 0) + anchorY?: number; // 纵向锚点比例(0~1,默认 0) + zIndex?: number; // 纵深层级(值越大越靠上) + + // 尺寸控制 + width?: number; // 元素宽度(单位:像素,默认 200) + height?: number; // 元素高度(单位:像素,默认 200) + + // 渲染控制 + filter?: string; // CSS滤镜(如 "blur(5px)") + hd?: boolean; // 启用高清画布(默认 true) + anti?: boolean; // 启用抗锯齿(默认 true) + noanti?: boolean; // 强制禁用抗锯齿(优先级高于 anti) + hidden?: boolean; // 隐藏元素(默认 false) + + // 变换与定位 + transform?: Transform; // 变换矩阵对象 + type?: RenderItemPosition; // 定位模式("static" | "absolute") + cache?: boolean; // 启用缓存优化(根据不同元素的特性有不同的值,多数情况下使用默认配置即可达到最优性能) + nocache?: boolean; // 强制禁用缓存(优先级高于 cache) + fall?: boolean; // 继承父元素变换矩阵(默认 false),不建议使用此参数,可能很快就会被删除 + + // 交互与样式 + id?: string; // 唯一标识符 + alpha?: number; // 不透明度(0~1,默认 1) + composite?: GlobalCompositeOperation; // 混合模式(如 "lighter") + cursor?: string; // 鼠标悬停样式(如 "pointer") + noevent?: boolean; // 禁用交互事件(默认 false) + + // 简写属性 + loc?: ElementLocator /* + [x, y, width?, height?, anchorX?, anchorY?] + 如果填写的话,两两一组要么都填要么都不填,也就是说元素数量需要是 2,4,6 个 + */; + anc?: ElementAnchor; // [anchorX, anchorY],如果填写的话,两项必填 + scale?: ElementScale; // [scaleX, scaleY],如果填写的话,两项必填 + rotate?: number; // 旋转弧度值(单位:弧度) +} +``` + +--- + +## 完整使用示例 + +```tsx +import { defineComponent } from 'vue'; +import { Transform } from '@motajs/render-core'; + +// 注意,以下属性均可选,按照自己需要填写即可,不需要的可以不用填,简单需求一般只需要修改定位 +// 而复杂需求可能需要填写更多的参数,但是基本不会出现所有参数都要填的场景 +// 编写 UI 的流程参考深度指南中的 UI 编写 +export const MyUI = defineComponent(() => { + return () => ( + + ); +}); +``` + +--- + +## 属性效果说明 + +| 属性组 | 关键效果 | +| -------------- | -------------------------------------------------------------------------- | +| **基础定位** | 元素将出现在 (100,200) 坐标,以中心点(锚点 0.5)为基准定位 | +| **尺寸控制** | 元素尺寸固定为 300x200 像素 | +| **渲染控制** | 应用阴影滤镜,高清画质,关闭抗锯齿实现像素风格 | +| **变换与定位** | 附加额外平移变换,使用绝对定位模式,禁用缓存优化 | +| **交互与样式** | 元素半透明,叠加混合模式,显示"move"光标,响应交互事件 | +| **简写属性** | 通过 loc 覆盖坐标和尺寸,anc 设置底部锚点,scale 实现拉伸/压缩,旋转 45 度 | + +--- + +## 注意事项 + +1. **优先级规则**: + - `noanti` > `anti`,`nocache` > `cache` + - 显式属性(如 `x`)与简写属性(如 `loc` 中的 `x`)相比,谁最后被设置,就用谁的 +2. **简写属性解析**: + ```ts + loc = [100, 200, 300, 200]; // → x=100, y=200, width=300, height=200 + anc = [0.5, 0]; // → anchorX=0.5, anchorY=0 + scale = [2]; // → scaleX=2, scaleY=2 + ``` +3. **其他注意事项** + - `transform.translate` 与 `x` `y` 和简写定位属性的 `x` `y` 完全等效,设置后者也会让 `transform` 的平移量改变 + - 如果不允许交互,那么光标也不会显示 + - 同 `zIndex` 下,后插入的元素会在上层,但是这也意味着如果是动态插入的元素(例如由于响应式更改而插入了一个新元素),会显示在后面代码的元素之上 +4. **常见问题** + - 参考 [指南](../../guide/ui-faq.md) diff --git a/docs/api/motajs-render-vue/GraphicBaseProps.md b/docs/api/motajs-render-vue/GraphicBaseProps.md new file mode 100644 index 0000000..697d347 --- /dev/null +++ b/docs/api/motajs-render-vue/GraphicBaseProps.md @@ -0,0 +1,167 @@ +# GraphicBaseProps API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +--- + +## 接口定义 + +```typescript +interface GraphicBaseProps extends BaseProps { + /** 是否填充(默认 false) */ + fill?: boolean; + /** 是否描边(默认 false) */ + stroke?: boolean; + /** 强制先描边后填充(优先级最高,默认 false) */ + strokeAndFill?: boolean; + /** 填充规则(默认 "evenodd") */ + fillRule?: CanvasFillRule; + /** 填充样式(颜色/渐变/图案) */ + fillStyle?: CanvasStyle; + /** 描边样式(颜色/渐变/图案) */ + strokeStyle?: CanvasStyle; + /** 交互时是否仅检测描边区域(默认 false) */ + actionStroke?: boolean; +} +``` + +--- + +## 核心属性说明 + +| 属性 | 类型 | 默认值 | 说明 | +| --------------- | ----------------------------------------------- | ----------- | -------------------------------------------------------------- | +| `fill` | `boolean` | `false` | 启用填充(需设置 `fillStyle`) | +| `stroke` | `boolean` | `false` | 启用描边(需设置 `strokeStyle` 和 `strokeWidth`) | +| `strokeAndFill` | `boolean` | `false` | 强制先描边后填充(覆盖 `fill` 和 `stroke` 的设置) | +| `fillRule` | `"nonzero"` \| `"evenodd"` | `"evenodd"` | 填充路径计算规则(影响复杂图形的镂空效果) | +| `fillStyle` | `string` \| `CanvasGradient` \| `CanvasPattern` | - | 填充样式(支持 CSS 颜色、渐变对象等) | +| `strokeStyle` | `string` \| `CanvasGradient` \| `CanvasPattern` | - | 描边样式 | +| `actionStroke` | `boolean` | `false` | 设为 `true` 时,交互事件仅响应描边区域(需配合 `stroke` 使用) | + +--- + +## 使用示例(以矩形为例) + +### 示例 1:仅填充模式 + +```tsx + +``` + +**效果**: + +- 200x150 矩形 +- 无描边效果 + +--- + +### 示例 2:仅描边模式 + +```tsx + +``` + +**交互特性**: + +- 4px 黑色半透明描边 +- 鼠标悬停在描边区域才会触发事件 + +--- + +### 示例 3:填充 + 描边(默认顺序) + +```tsx + +``` + +**渲染顺序**: + +1. 填充黄色背景 +2. 在填充层上绘制黑色描边 + +--- + +### 示例 4:强制先描边后填充 + +```tsx + +``` + +**渲染顺序**: + +1. 绘制紫色描边 +2. 在描边层上填充浅紫色 + **视觉效果**:描边被填充色覆盖一部分 + +--- + +## 最佳实践 + +### 交互增强技巧 + +```tsx +import { ref } from 'vue'; + +// 高亮描边交互反馈 +const hovered = ref(false); +// 使用 void 关键字屏蔽返回值,避免返回值泄漏 +const enter = () => void (hovered.value = true); +const leave = () => void (hovered.value = false); + +; +``` + +--- + +## 注意事项 + +1. **样式覆盖顺序**: + `strokeAndFill` 会强制按 **描边 → 填充** 顺序渲染,忽略 `fill` 和 `stroke` 的独立设置。 + +2. **路径闭合规则**: + `fillRule="evenodd"` 适用于以下场景: + + ```tsx + // 五角星镂空效果 + + ``` + +3. **性能问题**: + 多数情况下,图形的性能很好,不需要单独优化,但是如果你使用 `path` 标签,且内容复杂,建议添加 `cache` 属性来启用缓存,避免频繁的复杂图形绘制。 diff --git a/docs/api/motajs-render-vue/functions.md b/docs/api/motajs-render-vue/functions.md new file mode 100644 index 0000000..e52dc1a --- /dev/null +++ b/docs/api/motajs-render-vue/functions.md @@ -0,0 +1,149 @@ +# use.ts API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +--- + +## 函数说明 + +### `onTick` + +```typescript +function onTick(fn: (time: number) => void): void; +``` + +**功能**:注册每帧执行的回调(自动管理生命周期) +**推荐使用场景**:替代 `ticker` +**参数说明**: + +- `fn`: 接收当前时间戳的帧回调函数 + **示例**: + +```typescript +// Vue 组件中 +onTick(time => { + console.log('当前帧时间:', time); +}); +``` + +--- + +### `useAnimation` + +```typescript +function useAnimation(): [Animation]; +``` + +**功能**:创建动画实例(自动销毁资源) +**返回值**:包含动画实例的元组 +**推荐使用场景**:替代直接 `new Animation()` +**示例**: + +```typescript +const [anim] = useAnimation(); +anim.time(1000).move(100, 200); +``` + +--- + +### `useTransition` + +```typescript +function useTransition(): [Transition]; +``` + +**功能**:创建渐变实例(自动销毁资源) +**返回值**:包含渐变实例的元组 +**推荐使用场景**:替代直接 `new Transition()` +**示例**: + +```typescript +const [transition] = useTransition(); +transition.value.x = 10; +transition.time(500); +transition.value.x = 100; +``` + +--- + +### `useKey` + +```typescript +function useKey(noScope?: boolean): [Hotkey, symbol]; +``` + +**功能**:管理按键作用域(自动注销绑定) +**参数说明**: + +- `noScope`: 是否使用全局作用域(默认创建新作用域) + **返回值**:元组 [热键实例, 作用域标识] + **推荐使用场景**:替代直接操作全局热键实例 + **示例**: + +```typescript +const [hotkey, scope] = useKey(); +hotkey.realize('mykey_id', () => console.log('mykey_id emitted.')); +``` + +--- + +### `onEvent` + +```typescript +function onEvent< + T extends ERenderItemEvent, + K extends EventEmitter.EventNames +>( + item: RenderItem, + key: K, + listener: EventEmitter.EventListener +): void; +``` + +**功能**:自动管理事件监听生命周期 +**推荐使用场景**:替代直接 `item.on()` + 手动注销 +**示例**: + +```typescript +onEvent(sprite, 'click', event => { + console.log('元素被点击', event); +}); +``` + +--- + +## 总使用示例 + +```tsx +import { defineComponent } from 'vue'; +import { useAnimation, onTick, useKey } from '@motajs/render-vue'; + +export const MyComponent = defineComponent(() => { + // 动画控制 + const [anim] = useAnimation(); + anim.time(1000).rotate(Math.PI); + + // 帧循环 + onTick(time => { + console.log('当前游戏运行时间:', time); + }); + + // 按键控制 + const [hotkey, scope] = useKey(); + hotkey.realize('mykey_id', () => console.log('mykey_id emitted.')); + + return () => ; +}); +``` + +--- + +## 注意事项 + +1. **资源管理**:所有通过这些接口创建的资源(动画/渐变/事件)都会在组件卸载时自动销毁 +2. **内存安全**:使用原生接口可能导致内存泄漏,这些封装接口确保: + - 自动注销事件监听 + - 自动停止动画/渐变 + - 自动清理按键绑定 +3. **类型安全**:所有接口均包含完整的类型推断(如 `onEvent` 的事件类型检查) +4. **框架适配**:专为 Vue3 组合式 API 设计,不可用于其他框架环境 diff --git a/docs/api/motajs-render-vue/index.md b/docs/api/motajs-render-vue/index.md new file mode 100644 index 0000000..2cde0a7 --- /dev/null +++ b/docs/api/motajs-render-vue/index.md @@ -0,0 +1,3 @@ +# @motajs/render-vue + +目录: diff --git a/docs/api/motajs-render-vue/标签 container.md b/docs/api/motajs-render-vue/标签 container.md new file mode 100644 index 0000000..cf9e575 --- /dev/null +++ b/docs/api/motajs-render-vue/标签 container.md @@ -0,0 +1,139 @@ +# ContainerProps API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +--- + +```mermaid +graph LR + ContainerProps --> BaseProps + + click BaseProps "./BaseProps" +``` + +--- + +## 接口定义 + +```typescript +interface ContainerProps extends BaseProps {} // 无新增属性,完全继承 BaseProps +``` + +--- + +## 核心能力 + +1. **嵌套结构**:支持多层容器嵌套,构建复杂 UI 层级 +2. **批量更新**:通过容器隔离高频/低频更新内容 +3. **虚拟化支持**:推荐结合 `Scroll`/`Page` 组件处理大数据量场景 + +--- + +## 完整示例集 + +### 示例 1:基础容器(静态内容优化) + +```tsx +import { ref } from 'vue'; +import { Font } from '@motajs/render-style'; + +const count = ref(0); +const boldFont = new Font('Verdana', 18, 'px', 700); + +// 静态容器(启用缓存) + + {/* 静态背景 */} + + + {/* 动态计数器 */} + + + +; +``` + +**优化策略**: + +- 背景层缓存,避免重复绘制 +- 计数器单独放置在独立容器,避免高频内容污染致使低频内容也需要高频更新 + +--- + +### 示例 2:条件渲染 + 循环渲染 + +```tsx +import { ref } from 'vue'; + +// 根据状态显示不同内容 +const tab = ref<'list' | 'detail'>('list'); + +const item = [{ id: 0, name: '第一个元素' }]; + +return () => ( + + {/* 选项卡导航 */} + + + + + + {/* 条件内容区 */} + {tab === 'list' ? ( + {/* 循环渲染 */} + + {items.map((item, i) => ( + + ))} + + ) : ( + + + + )} + +); +``` + +--- + +### 示例 3:动态布局嵌套 + +```tsx +// 自适应居中布局 + + {/* 主内容卡片 */} + + + + + {/* 控制栏 */} + + + + +``` + +--- + +## 性能优化指南 + +### 缓存策略对比 + +| 场景 | 配置 | 重绘频率 | 适用场景 | +| ---------------- | -------------- | -------- | --------------- | +| 不频繁更新的内容 | `cache=true` | 偶尔重绘 | 背景/图标等 | +| 高频更新内容 | `nocache=true` | 每帧重绘 | 动画/计数器 | +| 混合内容 | 分层容器 | 按需更新 | 带静态背景的 UI | + +### 大数据量处理方案 + +考虑使用[滚动条](../user-client-modules/Scroll.md)或[分页](../user-client-modules/Page.md)组件。 + +--- + +## 注意事项 + +1. **缓存失效条件**:当任意子元素及自身发生变化时,将会自动触发更新 +2. **嵌套层级**:推荐使用容器嵌套提高缓存能力,但是并建议不嵌套过多 +3. **子元素更新**:修改容器子元素时会触发整个容器的缓存重建 +4. **内存管理**:超大缓存容器(如 4096x4096)可能触发浏览器纹理限制 diff --git a/docs/api/motajs-render-vue/标签 custom-container.md b/docs/api/motajs-render-vue/标签 custom-container.md new file mode 100644 index 0000000..2b2375a --- /dev/null +++ b/docs/api/motajs-render-vue/标签 custom-container.md @@ -0,0 +1,88 @@ +# ContainerCustomProps API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +--- + +```mermaid +graph LR + ContainerCustomProps --> ContainerProps --> BaseProps + + click ContainerProps "./ContainerProps" + click BaseProps "./BaseProps" +``` + +--- + +## 接口定义 + +```typescript +interface ContainerCustomProps extends ContainerProps { + /** + * 自定义容器渲染函数 + * @param canvas - 离屏画布对象(已应用容器的变换矩阵) + * @param children - 所有子元素(未经过滤,按照 zIndex 从小到大排列) + * @param transform - 容器自身相对于父元素的变换矩阵 + */ + render?: ( + canvas: MotaOffscreenCanvas2D, + children: RenderItem[], + transform: Transform + ) => void; +} +``` + +--- + +## 核心能力 + +1. **虚拟化渲染**:通过自定义筛选逻辑实现仅渲染想要渲染的内容 +2. **渲染劫持**:完全接管子元素的绘制流程 + +--- + +## 完整示例集 + +### 示例 1:默认渲染模式(等同普通容器) + +```tsx +// 不传 render 参数时,自动渲染全部子元素 + + + + + +``` + +--- + +### 示例 2:部分渲染(仅显示可见区域) + +```tsx +const render = ( + canvas: MotaOffscreenCanvas2D, + children: RenderItem[], + transform: Transform +) => { + // 在 [0, 0, 200, 200] 之外的内容不渲染 + children.forEach(child => { + const rect = child.getBoundingRect(); + // 不在范围内的排除 + if ( + rect.right < 0 || + rect.bottom < 0 || + rect.left > 200 || + rect.top > 200 + ) + return; + child.renderContent(canvas, transform); + }); +}; + + + {/* 循环渲染 */} + {items.map((item, i) => ( + + ))} +; +``` diff --git a/docs/api/motajs-render-vue/标签 g-bezier.md b/docs/api/motajs-render-vue/标签 g-bezier.md new file mode 100644 index 0000000..19fbac7 --- /dev/null +++ b/docs/api/motajs-render-vue/标签 g-bezier.md @@ -0,0 +1,202 @@ +# g-bezier 标签 API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +--- + +```mermaid +graph LR + BezierProps --> GraphicBaseProps --> BaseProps + + click GraphicBaseProps "./GraphicBaseProps" + click BaseProps "./BaseProps" +``` + +--- + +## 接口定义 + +```typescript +interface BezierProps extends GraphicBaseProps { + sx?: number; // 起点X坐标 + sy?: number; // 起点Y坐标 + cp1x?: number; // 第一控制点X坐标 + cp1y?: number; // 第一控制点Y坐标 + cp2x?: number; // 第二控制点X坐标 + cp2y?: number; // 第二控制点Y坐标 + ex?: number; // 终点X坐标 + ey?: number; // 终点Y坐标 + curve?: BezierParams; // 简写属性 [sx, sy, cp1x, cp1y, cp2x, cp2y, ex, ey] +} +``` + +--- + +## 核心属性说明 + +| 属性 | 类型 | 默认值 | 说明 | +| ------- | ------------------------------------------ | ------ | ------------------------------------- | +| `sx` | `number` | - | 曲线起点 X 坐标(单位:像素) | +| `sy` | `number` | - | 曲线起点 Y 坐标(单位:像素) | +| `cp1x` | `number` | - | 第一控制点 X 坐标(影响曲线起始方向) | +| `cp1y` | `number` | - | 第一控制点 Y 坐标 | +| `cp2x` | `number` | - | 第二控制点 X 坐标(影响曲线结束方向) | +| `cp2y` | `number` | - | 第二控制点 Y 坐标 | +| `ex` | `number` | - | 曲线终点 X 坐标 | +| `ey` | `number` | - | 曲线终点 Y 坐标 | +| `curve` | `[sx, sy, cp1x, cp1y, cp2x, cp2y, ex, ey]` | - | 简写属性:一次性定义全部坐标点 | + +--- + +## 完整示例集 + +### 示例 1:基础三次贝塞尔曲线 + +```tsx +// 曲线默认仅描边,不需要单独设置 stroke 属性 + +``` + +--- + +### 示例 2:虚线波浪线 + +```tsx + +``` + +**曲线形态**: + +- 起点 (50,200) → 第一控制点 (150,50) +- 第二控制点 (250,350) → 终点 (350,200) +- 形成"S"型波浪线 + +--- + +### 示例 3:动态流体效果 + +```tsx +import { ref } from 'vue'; + +const offset = ref(0); + +// 每帧更新控制点位置 +onTick(() => { + offset.value += 0.02; +}); + +; +``` + +--- + +## 控制点行为说明 + +### 控制点影响示意图 + +```typescript +/* + (cp1) + ▲ + | +(start)●━━╋━━━━━━━━━━━┓ + ┃ ┃ + ┃ (cp2) ┃ + ┃ ▼ ┃ + ┗━━━━━━━●━━(end) +*/ +``` + +- **第一控制点** (`cp1`):控制曲线起始方向的弯曲程度 +- **第二控制点** (`cp2`):控制曲线结束方向的弯曲程度 + +### 特殊形态案例 + +| 控制点布局 | 曲线形态描述 | +| ------------------------------------ | ----------------- | +| `cp1`靠近起点,`cp2`靠近终点 | 近似直线 | +| `cp1`与`cp2`对称分布 | 形成对称波浪 | +| `cp1`在起点正上方,`cp2`在终点正下方 | 创建垂直"S"型曲线 | + +--- + +## 高级用法示例 + +### 复杂路径组合 + +```tsx +// 组合多条贝塞尔曲线形成花瓣造型 + + {Array.from({ length: 5 }).map((_, i) => ( + + ))} + +``` + +--- + +## 注意事项 + +1. **闭合路径**: + 三次贝塞尔曲线默认不闭合,如需闭合需手动连接到起点: + +```tsx + +``` + +2. **控制点极值**: + 当控制点距离起点/终点过远时可能产生剧烈弯曲: + +```tsx +// 可能产生非预期锐角 + +``` diff --git a/docs/api/motajs-render-vue/标签 g-circle.md b/docs/api/motajs-render-vue/标签 g-circle.md new file mode 100644 index 0000000..412ec79 --- /dev/null +++ b/docs/api/motajs-render-vue/标签 g-circle.md @@ -0,0 +1,182 @@ +# g-circle 标签 API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +--- + +```mermaid +graph LR + CircleProps --> GraphicBaseProps --> BaseProps + + click GraphicBaseProps "./GraphicBaseProps" + click BaseProps "./BaseProps" +``` + +--- + +## 接口定义 + +```typescript +interface CircleProps extends GraphicBaseProps { + radius?: number; // 圆的半径 + start?: number; // 起始角度(单位:弧度) + end?: number; // 结束角度(单位:弧度) + circle?: CircleParams; // 简写属性 [x, y, radius, start?, end?],后两项要么都填,要么都不填 +} +``` + +--- + +## 核心属性说明 + +| 属性 | 类型 | 默认值 | 说明 | +| -------- | ------------------------- | ------ | ---------------------------------------------------------- | +| `radius` | `number` | - | 圆的半径(单位:像素) | +| `start` | `number` | `0` | 起始角度(弧度),0 弧度 = 3 点钟方向(水平向右) | +| `end` | `number` | `2π` | 结束角度(弧度),默认完整圆 | +| `circle` | `[x, y, r, start?, end?]` | - | 简写属性:`[圆心x, 圆心y, 半径, 起始角度?, 结束角度?]` | + +--- + +## 完整示例集 + +### 示例 1:基础圆形(填充) + +```tsx + +``` + +--- + +### 示例 2:描边圆形 + +```tsx + +``` + +--- + +### 示例 3:弓形(填充闭合) + +```tsx + +``` + +**角度说明**: + +- 绘制方向:**顺时针**(从 12 点走向 9 点) +- 自动闭合路径形成扇形 + +--- + +### 示例 4:圆弧(非闭合) + +```tsx + +``` + +**路径特征**: + +- 起始角度:45 度(右上对角线方向) +- 结束角度:270 度(12 点钟方向) +- 开环不闭合,形成月牙形弧线 + +--- + +## 角度系统详解 + +### 坐标系与方向 + +```typescript +// 0弧度基准点与绘制方向示意图 +/* + Math.PI / 2 (90°) + | + | +Math.PI (180°) ——+—— 0 (0°) + | + | + Math.PI * 3 / 2 (270°) +*/ +``` + +- **0 弧度基准**:3 点钟方向(与浏览器 Canvas API 完全一致) +- **绘制方向**:角度递增为顺时针方向(`start=0, end=Math.PI/2` 绘制右下四分之一圆) + +### 特殊角度对照表 + +| 弧度值 | 角度值 | 方向 | +| ------------- | ------ | ------ | +| `0` | 0° | 正右 → | +| `Math.PI/2` | 90° | 正上 ↑ | +| `Math.PI` | 180° | 正左 ← | +| `3*Math.PI/2` | 270° | 正下 ↓ | + +--- + +## 高级用法示例 + +### 动态进度环 + +```tsx +import { ref } from 'vue'; +import { onTick } from '@motajs/render-vue'; + +const progress = ref(0); + +// 每帧更新进度 +onTick(() => { + progress.value += 0.01; +}); + +; +``` + +**效果**: + +- 0%时从顶部开始 +- 进度条顺时针增长 +- 圆角端点消除锯齿 + +--- + +## 注意事项 + +1. **简写属性优先级**: + +```tsx +// 谁最后设置用谁的 + +``` diff --git a/docs/api/motajs-render-vue/标签 g-ellipse.md b/docs/api/motajs-render-vue/标签 g-ellipse.md new file mode 100644 index 0000000..877b73e --- /dev/null +++ b/docs/api/motajs-render-vue/标签 g-ellipse.md @@ -0,0 +1,175 @@ +# g-ellipse 标签 API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +--- + +```mermaid +graph LR + EllipseProps --> GraphicBaseProps --> BaseProps + + click GraphicBaseProps "./GraphicBaseProps" + click BaseProps "./BaseProps" +``` + +--- + +## 接口定义 + +```typescript +interface EllipseProps extends GraphicBaseProps { + radiusX?: number; // 椭圆X轴半径 + radiusY?: number; // 椭圆Y轴半径 + start?: number; // 起始角度(单位:弧度) + end?: number; // 结束角度(单位:弧度) + ellipse?: EllipseParams; // 简写属性 [x, y, rx, ry, start?, end?] +} +``` + +--- + +## 核心属性说明 + +| 属性 | 类型 | 默认值 | 说明 | +| --------- | ------------------------------ | ------ | ------------------------------------------------------------------ | +| `radiusX` | `number` | - | 椭圆 X 轴半径(单位:像素) | +| `radiusY` | `number` | - | 椭圆 Y 轴半径(单位:像素) | +| `start` | `number` | `0` | 起始角度(弧度),0 弧度 = 3 点钟方向 | +| `end` | `number` | `2π` | 结束角度(弧度),默认完整椭圆 | +| `ellipse` | `[x, y, rx, ry, start?, end?]` | - | 简写属性:`[圆心x, 圆心y, X半径, Y半径, 起始角度?, 结束角度?]` | + +--- + +## 完整示例集 + +### 示例 1:基础椭圆(填充) + +```tsx + +``` + +--- + +### 示例 2:描边椭圆 + +```tsx + +``` + +--- + +### 示例 3:椭圆弓形(闭合) + +```tsx + +``` + +**角度说明**: + +- 绘制方向:**顺时针**(从 45 度到 225 度) +- 自动闭合路径形成扇形 +- 若路径自相交,`evenodd` 规则会生成镂空 + +--- + +### 示例 4:椭圆弧线(非闭合) + +```tsx + +``` + +**路径特征**: + +- 垂直方向椭圆弧(X 半径 > Y 半径) +- 起始角度:-90 度(12 点方向) +- 结束角度:90 度(6 点方向) +- 开环形成对称弧线 + +--- + +## 角度系统详解 + +### 坐标系与方向 + +```typescript +// 角度系统与圆形一致,但受半径比例影响: +/* + radiusY + ↑ + | + radiusX +---→ +*/ +``` + +- **0 弧度基准**:3 点钟方向(与浏览器 Canvas API 一致) +- **半径影响**:当 `radiusX ≠ radiusY` 时,相同角度对应的端点位置会拉伸 +- **绘制方向**:角度递增为顺时针方向 + +### 特殊角度效果 + +| 参数组合 | 效果描述 | +| ---------------------------------- | ------------------------ | +| `radiusX=radiusY` | 退化为标准圆形 | +| `start=0, end=Math.PI` | 右半椭圆(水平方向半圆) | +| `start=Math.PI/2, end=3*Math.PI/2` | 上半椭圆(垂直方向半圆) | + +--- + +## 高级用法示例 + +### 动态仪表盘 + +```tsx +import { ref } from 'vue'; + +const value = ref(0.3); + +; +``` + +**效果**: + +- 椭圆弧仪表盘,从左上方向右侧延伸 +- 进度值 `0.3` 时覆盖 30%路径 + +--- + +## 注意事项 + +1. **简写属性优先级**: + +```tsx +// 谁最后被设置用谁的 + +``` diff --git a/docs/api/motajs-render-vue/标签 g-line.md b/docs/api/motajs-render-vue/标签 g-line.md new file mode 100644 index 0000000..b88bff2 --- /dev/null +++ b/docs/api/motajs-render-vue/标签 g-line.md @@ -0,0 +1,125 @@ +# g-line 标签 API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +--- + +```mermaid +graph LR + LineProps --> GraphicBaseProps --> BaseProps + + click GraphicBaseProps "./GraphicBaseProps" + click BaseProps "./BaseProps" +``` + +--- + +## 接口定义 + +```typescript +interface LineProps extends GraphicBaseProps { + x1?: number; // 起点X坐标 + y1?: number; // 起点Y坐标 + x2?: number; // 终点X坐标 + y2?: number; // 终点Y坐标 + line?: LineParams; // 简写属性 [x1, y1, x2, y2] +} +``` + +--- + +## 核心属性说明 + +| 属性 | 类型 | 默认值 | 说明 | +| ---------- | ----------------------------------- | -------- | ---------------------------------------------- | +| `x1` | `number` | - | 线段起点 X 坐标(单位:像素) | +| `y1` | `number` | - | 线段起点 Y 坐标(单位:像素) | +| `x2` | `number` | - | 线段终点 X 坐标(单位:像素) | +| `y2` | `number` | - | 线段终点 Y 坐标(单位:像素) | +| `line` | `[x1, y1, x2, y2]` | - | 简写属性:一次性定义起点和终点坐标 | +| `lineDash` | `number[]` | - | 虚线模式(如 `[5, 3]` 表示 5px 实线+3px 间隙) | +| `lineCap` | `"butt"` \| `"round"` \| `"square"` | `"butt"` | 线段端点样式 | + +--- + +## 完整示例集 + +### 示例 1:基础实线 + +```tsx +// 线段默认就是仅描边,因此不需要单独设置 stroke 属性 + +``` + +--- + +### 示例 2:虚线线段 + +```tsx + +``` + +**效果说明**: + +- 水平红色虚线 +- 线段端点呈圆形 + +--- + +### 示例 3:动态线段(动画) + +```tsx +import { transitioned } from '@user/client-modules'; + +// 创建渐变 +const x2 = transitioned(100, 2000, linear()); +x2.set(400); // 终点横坐标从 100 变到 400 + +return () => ( + +); +``` + +**动态效果**: + +- 线段从 100px 位置向右延伸至 400px +- 2 秒完成动画 + +--- + +## 线段样式对照表 + +| 样式组合 | 效果图示 | +| --------------------- | ----------------- | +| `lineCap="butt"` | 平头端点:⎯ | +| `lineCap="round"` | 圆头端点:⭘―――――⭘ | +| `lineCap="square"` | 方头端点:▯―――――▯ | +| `lineDash=[20,5,5,5]` | 复杂虚线:━━⧀┄⧀┄ | + +--- + +## 注意事项 + +1. **坐标系差异**: + 线段坐标基于父容器坐标系,如需相对定位建议嵌套在`container`中: + +```tsx + + {/* 实际坐标为 (100,100)→(150,150) */} + + +``` diff --git a/docs/api/motajs-render-vue/标签 g-path.md b/docs/api/motajs-render-vue/标签 g-path.md new file mode 100644 index 0000000..30cb870 --- /dev/null +++ b/docs/api/motajs-render-vue/标签 g-path.md @@ -0,0 +1,173 @@ +# g-path 标签 API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +--- + +```mermaid +graph LR + PathProps --> GraphicBaseProps --> BaseProps + + click GraphicBaseProps "./GraphicBaseProps" + click BaseProps "./BaseProps" +``` + +--- + +## 接口定义 + +```typescript +interface PathProps extends GraphicBaseProps { + path?: Path2D; // 自定义路径对象 +} +``` + +--- + +## 核心属性说明 + +| 属性 | 类型 | 默认值 | 说明 | +| -------------- | -------------------------- | ----------- | -------------------------------------------------- | +| `path` | `Path2D` | - | 自定义矢量路径(支持多路径、贝塞尔曲线等复杂形状) | +| `fillRule` | `"nonzero"` \| `"evenodd"` | `"evenodd"` | 填充规则(影响路径重叠区域的渲染) | +| `actionStroke` | `boolean` | `false` | 设为 `true` 时,交互事件仅检测描边区域 | + +--- + +## 完整示例集 + +### 示例 1:复杂星形路径 + +```tsx +// 创建五角星路径 +const starPath = new Path2D(); +for (let i = 0; i < 5; i++) { + const angle = (i * 2 * Math.PI) / 5 - Math.PI / 2; + const x = 100 + Math.cos(angle) * 50; + const y = 100 + Math.sin(angle) * 50; + if (i === 0) starPath.moveTo(x, y); + else starPath.lineTo(x, y); +} +starPath.closePath(); + +; +``` + +**效果说明**: + +- 使用`evenodd`规则自动产生星形镂空效果 +- 金色填充+橙色描边的五角星 + +--- + +### 示例 2:交互式描边检测 + +```tsx +import { ref } from 'vue'; +const [clicked, setClicked] = useState(false); + +const clicked = ref(false); + +// 创建对话气泡路径 +const bubblePath = new Path2D(); +bubblePath.moveTo(50, 20); +bubblePath.quadraticCurveTo(25, 0, 0, 20); +bubblePath.quadraticCurveTo(25, 40, 50, 20); +bubblePath.rect(0, 20, 200, 100); + +const click = () => void (clicked.value = !click.value); + +; +``` + +**交互特性**: + +- 点击描边区域切换填充颜色 +- 内部区域不响应点击事件 + +--- + +### 示例 3:组合路径(齿轮造型) + +```tsx +const gearPath = new Path2D(); +// 主体圆形 +gearPath.arc(100, 100, 80, 0, Math.PI * 2); +// 添加8个齿牙 +for (let i = 0; i < 8; i++) { + const angle = ((Math.PI * 2) / 8) * i; + gearPath.moveTo(100 + Math.cos(angle) * 90, 100 + Math.sin(angle) * 90); + gearPath.lineTo( + 100 + Math.cos(angle + Math.PI / 8) * 110, + 100 + Math.sin(angle + Math.PI / 8) * 110 + ); + gearPath.lineTo( + 100 + Math.cos(angle - Math.PI / 8) * 110, + 100 + Math.sin(angle - Math.PI / 8) * 110 + ); +} + +; +``` + +--- + +## 高级用法示例 + +### SVG 路径转换 + +```tsx +// 将SVG路径转换为Path2D +const svgPath = 'M10 80 C 40 10, 65 10, 95 80 S 150 150, 180 80'; +const path = new Path2D(svgPath); + +; +``` + +--- + +## 注意事项 + +1. **路径闭合**: + + ```tsx + // 必须显式闭合路径才能正确填充 + const path = new Path2D(); + path.moveTo(0, 0); + path.lineTo(100, 0); + path.lineTo(100, 100); + path.closePath(); // 关键闭合操作 + ``` diff --git a/docs/api/motajs-render-vue/标签 g-quad.md b/docs/api/motajs-render-vue/标签 g-quad.md new file mode 100644 index 0000000..eff64ac --- /dev/null +++ b/docs/api/motajs-render-vue/标签 g-quad.md @@ -0,0 +1,119 @@ +# g-quad 标签 API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +--- + +```mermaid +graph LR + QuadraticProps --> GraphicBaseProps --> BaseProps + + click GraphicBaseProps "./GraphicBaseProps" + click BaseProps "./BaseProps" +``` + +--- + +## 接口定义 + +```typescript +interface QuadraticProps extends GraphicBaseProps { + sx?: number; // 起点X坐标 + sy?: number; // 起点Y坐标 + cpx?: number; // 控制点X坐标 + cpy?: number; // 控制点Y坐标 + ex?: number; // 终点X坐标 + ey?: number; // 终点Y坐标 + curve?: QuadParams; // 简写属性 [sx, sy, cpx, cpy, ex, ey] +} +``` + +--- + +## 核心属性说明 + +| 属性 | 类型 | 默认值 | 说明 | +| ------- | ---------------------------- | ------ | --------------------------------------- | +| `sx` | `number` | - | 曲线起点 X 坐标(单位:像素) | +| `sy` | `number` | - | 曲线起点 Y 坐标(单位:像素) | +| `cpx` | `number` | - | 控制点 X 坐标(决定曲线弯曲方向和程度) | +| `cpy` | `number` | - | 控制点 Y 坐标 | +| `ex` | `number` | - | 曲线终点 X 坐标 | +| `ey` | `number` | - | 曲线终点 Y 坐标 | +| `curve` | `[sx, sy, cpx, cpy, ex, ey]` | - | 简写属性:一次性定义全部坐标点 | + +--- + +## 完整示例集 + +### 示例 1:基础二次贝塞尔曲线 + +```tsx + +``` + +--- + +### 示例 2:虚线抛物线 + +```tsx + +``` + +**曲线形态**: + +- 起点 (50,400) → 控制点 (250,100) → 终点 (450,400) +- 形成对称的类似抛物线形状的曲线 + +--- + +## 控制点行为说明 + +### 控制点影响示意图 + +```typescript +/* + (cpx,cpy) + ● + / \ + / \ +(start) ●-----●(end) +*/ +``` + +- **单控制点**:二次贝塞尔曲线仅有一个控制点,同时影响曲线的起始和结束方向 +- **对称性**:控制点距离起点/终点的垂直距离越大,曲线弯曲越明显 + +### 特殊形态案例 + +| 控制点布局 | 曲线形态描述 | +| ------------------------ | ------------- | +| 控制点在起点终点连线中点 | 退化为直线 | +| 控制点在起点正上方 | 形成"U"型曲线 | +| 控制点在终点右侧 | 形成"C"型曲线 | + +--- + +## 注意事项 + +1. **控制点极限值**: + 当控制点与起点/终点距离过大时可能产生锐角: + +```tsx +// 可能产生非预期的尖角形态 + +``` diff --git a/docs/api/motajs-render-vue/标签 g-rect.md b/docs/api/motajs-render-vue/标签 g-rect.md new file mode 100644 index 0000000..86f4143 --- /dev/null +++ b/docs/api/motajs-render-vue/标签 g-rect.md @@ -0,0 +1,91 @@ +# g-rect 标签 API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +--- + +```mermaid +graph LR + RectProps --> GraphicBaseProps --> BaseProps + + click GraphicBaseProps "./GraphicBaseProps" + click BaseProps "./BaseProps" +``` + +--- + +## 使用示例 + +### 示例 1:仅填充模式 + +```tsx + +``` + +**效果**: + +- 200x150 矩形 +- 无描边效果 + +--- + +### 示例 2:仅描边模式 + +```tsx + +``` + +**交互特性**: + +- 4px 黑色半透明描边 +- 鼠标悬停在描边区域才会触发事件 + +--- + +### 示例 3:填充 + 描边(默认顺序) + +```tsx + +``` + +**渲染顺序**: + +1. 填充黄色背景 +2. 在填充层上绘制黑色描边 + +--- + +### 示例 4:强制先描边后填充 + +```tsx + +``` + +**渲染顺序**: + +1. 绘制紫色描边 +2. 在描边层上填充浅紫色 + **视觉效果**:描边被填充色覆盖一部分 diff --git a/docs/api/motajs-render-vue/标签 g-rectr.md b/docs/api/motajs-render-vue/标签 g-rectr.md new file mode 100644 index 0000000..f245ef3 --- /dev/null +++ b/docs/api/motajs-render-vue/标签 g-rectr.md @@ -0,0 +1,167 @@ +# g-rectr 标签 API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +--- + +```mermaid +graph LR + RectRProps --> GraphicBaseProps --> BaseProps + + click GraphicBaseProps "./GraphicBaseProps" + click BaseProps "./BaseProps" +``` + +--- + +## 接口定义 + +```typescript +interface RectRProps extends GraphicBaseProps { + /** + * 圆形圆角参数 [左上, 右上, 右下, 左下] + * - 1个值:全角相同 + * - 2个值:左上+右下 / 右上+左下 + * - 3个值:左上 / 右上+左下 / 右下 + * - 4个值:分别设置四个角 左上、右上、左下、右下 + */ + circle?: RectRCircleParams; + + /** + * 椭圆圆角参数 [x半径, y半径, ...] + * - 1组:全角相同 + * - 2组:左上+右下 / 右上+左下 + * - 3组:左上 / 右上+左下 / 右下 + * - 4组:分别设置四个角 左上、右上、左下、右下 + */ + ellipse?: RectREllipseParams; +} +``` + +--- + +## 核心行为规则 + +- **参数限制**:若圆角值超过 `width/height` 的 50% 或为负数,将自动修正: + - 负值 → 修正为 0 + - 超过 50% → 修正为 50% +- **参数优先级**:`ellipse` 优先级高于 `circle` + +--- + +## 完整示例集 + +### 示例 1:统一圆形圆角 + +```tsx + +``` + +--- + +### 示例 2:差异圆形圆角 + +```tsx + +``` + +--- + +### 示例 3:椭圆圆角 + +```tsx + +``` + +--- + +### 示例 4:混合椭圆圆角 + +```tsx + +``` + +--- + +## 参数对照表 + +### 圆形圆角(circle)参数规则 + +| 参数数量 | 生效规则 | +| -------- | -------------------------------------------- | +| 1 | 全角相同:`[20] → [20,20,20,20]` | +| 2 | 对角对称:`[10,30] → [10,30,30,10]` | +| 3 | 左上/对角/右下:`[10,20,30] → [10,20,30,20]` | +| 4 | 独立设置四个角:`[10,20,30,40]` | + +### 椭圆圆角(ellipse)参数规则 + +| 参数组数 | 生效规则 | +| -------- | ---------------------------------------------- | +| 1 | 全角相同:`[15,20] → 四角均为15x20` | +| 2 | 对角对称:`[10,5,20,10] → 左上+右下/右上+左下` | +| 3 | 左上/对角/右下:`[10,20,15,5,20,10]` | +| 4 | 独立设置四个角:`[10,20,15,25,20,30,5,10]` | + +--- + +## 错误处理示例 + +```tsx +// 危险参数示例 + +``` + +**实际生效值**: + +- 负值修正:`-10 → 0` +- 超限修正:`200 → min(200, 50/2=25) → 25px` +- 最终参数:`[0,25] → [0,25,25,0]` + +--- + +## 最佳实践建议 + +1. **响应式圆角**: + +```tsx +// 圆角随尺寸变化 + +``` diff --git a/docs/api/motajs-render-vue/标签 icon.md b/docs/api/motajs-render-vue/标签 icon.md new file mode 100644 index 0000000..f70b128 --- /dev/null +++ b/docs/api/motajs-render-vue/标签 icon.md @@ -0,0 +1,121 @@ +# icon 标签 API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +--- + +```mermaid +graph LR + IconProps --> BaseProps + + click BaseProps "./BaseProps" +``` + +--- + +## 接口定义 + +```typescript +interface IconProps extends BaseProps { + icon: AllNumbers | AllIds; // 图标ID或数字标识 + frame?: number; // 显示指定帧(从0开始计数) + animate?: boolean; // 是否启用帧动画循环(默认false) +} +``` + +--- + +## 核心属性说明 + +| 属性 | 类型 | 默认值 | 说明 | +| --------- | -------------------- | -------- | ------------------------------------------------------ | +| `icon` | `number` \| `string` | **必填** | 图标资源标识符(对应素材库中的图块 ID 或预设数字编码) | +| `frame` | `number` | `0` | 指定显示的帧序号(当`animate=false`时生效) | +| `animate` | `boolean` | `false` | 启用自动播放帧动画(优先级高于`frame`参数) | + +--- + +## 完整示例集 + +### 示例 1:静态显示指定帧 + +```tsx +// 显示图块ID为"greenSlime"的第3帧(索引从0开始) + +``` + +--- + +### 示例 2:动态帧动画 + +```tsx +// 自动播放4帧循环动画 + +``` + +**动画行为**: + +- 播放图标自带的 4 帧动画序列(0→1→2→3→0...) +- 动画速度由素材预设帧率决定 + +--- + +### 示例 3:交互控制动画 + +```tsx +import { ref } from 'vue'; + +const animating = ref(false); + +const click = () => void (animating.value = false); + +// 点击切换动画状态 +; +``` + +**交互逻辑**: + +- 初始状态显示第 1 帧(门关闭) +- 点击后播放开门动画 + +--- + +## 帧动画系统规则 + +### 帧索引定义 + +```typescript +// 图块素材帧结构示例 +/* + [0] [1] [2] [3] + +---+---+---+---+ + | | | | | // 4帧水平排列的图块素材 + +---+---+---+---+ +*/ +``` + +- **播放方向**:始终从 `0` 帧开始正向循环 +- **循环模式**:播放到最后一帧后回到第 `0` 帧 + +### 参数限制 + +| 场景 | 系统行为 | +| ----------------------------- | ------------------------------------- | +| `frame` 超过最大帧数 | 报错 | +| `frame` 为负数 | 报错 | +| `animate=true` 时修改 `frame` | `frame` 参数被忽略,始终从 0 开始播放 | diff --git a/docs/api/motajs-render-vue/标签 image.md b/docs/api/motajs-render-vue/标签 image.md new file mode 100644 index 0000000..5429cfd --- /dev/null +++ b/docs/api/motajs-render-vue/标签 image.md @@ -0,0 +1,103 @@ +# image 标签 API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +--- + +```mermaid +graph LR + ImageProps --> BaseProps + + click BaseProps "./BaseProps" +``` + +--- + +## 接口定义 + +```typescript +interface ImageProps extends BaseProps { + /** 必填 - 图片对象(CanvasImageSource 类型) */ + image: CanvasImageSource; +} +``` + +--- + +## 核心功能 + +- **尺寸控制**:通过 `width/height` 或 `loc` 简写属性定义图片尺寸(默认 200x200) +- **像素风支持**:通过 `noanti=true` 禁用抗锯齿 + +--- + +## 使用示例 + +### 示例 1:基础图片显示 + +```tsx +const image = core.material.images.images['myimage.webp']; +// 显示 200x200 的默认尺寸图片 +; +``` + +**等效简写**: + +```tsx +const image = core.material.images.images['myimage.webp']; + +; +``` + +--- + +### 示例 2:自定义尺寸 + +```tsx +const image = core.material.images.images['myimage.webp']; +// 方式一:直接设置宽高 +; + +// 方式二:通过 loc 简写属性 +; +``` + +--- + +### 示例 3:像素风渲染(禁用抗锯齿) + +```tsx +const pixelImage = core.material.images.images['myimage.webp']; + +// 硬核像素风格配置 +; +``` + +**效果说明**: + +- 原始 32x32 像素图 → 放大为 64x64 像素 +- 每个像素块保持锐利边缘 + +--- + +## 属性配置表 + +| 属性 | 类型 | 默认值 | 说明 | +| ------- | ------------------- | -------- | ------------------------------------------- | +| `image` | `CanvasImageSource` | **必填** | 图片资源(ImageBitmap/HTMLImageElement 等) | diff --git a/docs/api/motajs-render-vue/标签 sprite.md b/docs/api/motajs-render-vue/标签 sprite.md new file mode 100644 index 0000000..d44b0d0 --- /dev/null +++ b/docs/api/motajs-render-vue/标签 sprite.md @@ -0,0 +1,148 @@ +# sprite 标签 API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +--- + +```mermaid +graph LR + SpriteProps --> BaseProps + + click BaseProps "./BaseProps" +``` + +--- + +## 接口定义 + +```typescript +interface SpriteProps extends BaseProps { + /** + * 自定义渲染函数 + * @param canvas - 离屏画布对象 + * @param transform - 当前元素相对于父元素的变换矩阵 + */ + render?: (canvas: MotaOffscreenCanvas2D, transform: Transform) => void; +} +``` + +--- + +## 核心能力 + +通过 `render` 函数实现 **动态绘制**,可结合: + +- 基础定位/变换参数(继承 `BaseProps`) +- 动画系统(`useAnimation`) +- 帧回调(`onTick`) +- 自定义图形绘制(路径/滤镜/混合模式) + +**注意**,这个标签虽然非常基础,但是应该并不常用,因为很多内容都有对应的标签可以实现(例如线可以使用 `g-line` 标签等),因此如果你在考虑使用此标签,请确认你必须使用它,或是你的场景对性能非常敏感。 + +--- + +## 使用示例 + +以下的示例代码均应该在**组件内部**编写,哪里是组件内部请参考[指南](../../guide/ui.md)。 + +### 示例 1:基础图形 + +```tsx +import { MotaOffscreenCanvas2D } from '@motajs/render-core'; + +// 绘制旋转了 45 度的彩色方块 +const render = (canvas: MotaOffscreenCanvas2D) => { + const ctx = canvas.ctx; + ctx.fillStyle = 'rgba(255,0,0,0.8)'; + ctx.strokeStyle = 'blue'; + ctx.lineWidth = 3; + ctx.beginPath(); + ctx.rect(0, 0, 200, 200); + ctx.fill(); + ctx.stroke(); +}; + +; +``` + +**效果**: + +- 100x100 红色方块,中心点位于 (200,200) +- 45 度旋转,蓝色描边 +- 80% 不透明度 + +--- + +### 示例 2:结合动画系统 + +```tsx +import { ref } from 'vue'; +import { useAnimation } from '@motajs/render-vue'; + +// 平移动画 + 动态缩放 +const [anim] = useAnimation(); +anim.time(2000).move(100, 0).scale(1.5); + +const loc = ref([0, 0]); +const scale = ref([1, 1]); + +onTick(() => { + loc.value = [anim.x, anim.y]; + scale.value = [anim.size, anim.size]; +}); + +const render = canvas => { + const ctx = canvas.ctx; + ctx.rect(0, 0, 200, 200); + ctx.fill(); +}; + +return () => ; +``` + +**效果**: + +- 矩形横向放大 1.5 倍,横向位置移动 100px +- 2 秒线性动画 + +--- + +### 示例 3:交互事件 + 滤镜 + +```tsx +import { ref } from 'vue'; + +// 悬浮模糊 +const filter = ref('none'); + +const enter = () => { + filter.value = 'blur(2px)'; +}; +const leave = () => { + filter.value = 'none'; +}; + +return () => ( + +); +``` + +**效果**: + +- 鼠标悬浮时添加模糊滤镜 +- 鼠标悬浮显示指针光标 + +--- + +## 注意事项 + +1. **坐标系**:`render` 函数内使用 **局部坐标系**,锚点变换已自动处理 +2. **循环更新**:避免在 `render` 中循环更新自身 diff --git a/docs/api/motajs-render-vue/标签 text.md b/docs/api/motajs-render-vue/标签 text.md new file mode 100644 index 0000000..50fb216 --- /dev/null +++ b/docs/api/motajs-render-vue/标签 text.md @@ -0,0 +1,160 @@ +# TextProps API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +--- + +```mermaid +graph LR + TextProps --> BaseProps + + click BaseProps "./BaseProps" +``` + +--- + +## 接口定义 + +```typescript +interface TextProps extends BaseProps { + text?: string; // 显示的文字内容 + fillStyle?: CanvasStyle; // 文字填充样式(颜色/渐变/图案) + strokeStyle?: CanvasStyle; // 文字描边样式 + font?: Font; // 字体配置对象 + strokeWidth?: number; // 描边宽度(单位:像素) +} +``` + +--- + +## 使用示例 + +### 1. 基本文本渲染 + +```tsx +import { Font } from '@motajs/render-style'; + +; +``` + +**效果**: + +- 在 (100,50) 位置显示蓝色 "Hello World" +- 使用 24px 粗体 Arial 字体 +- 无描边效果 + +--- + +### 2. 描边文字 + 填充组合 + +```tsx +import { Font } from '@motajs/render-style'; + +; +``` + +**效果**: + +- 金色文字带黑色半透明描边 +- 32px 加粗斜体 Verdana +- 2px 描边宽度 + +--- + +### 3. 动态更新文本 + +```tsx +import { ref } from 'vue'; +import { Font } from '@motajs/render-style'; + +// Vue3 组件示例 +const count = ref(0); + +onTick(() => { + count.value++; +}); + +return () => ( + +); +``` + +--- + +## 高级用法示例 + +### 文字路径动画 + +```tsx +import { ref } from 'vue'; +import { Font, onTick } from '@motajs/render'; + +const offset = ref(0); + +onTick(() => { + offset.value = (offset.value + 1) % 100; +}); + +; +``` + +--- + +### 文字阴影 + 滤镜 + +```tsx +import { Font } from '@motajs/render-style'; + +; +``` + +--- + +## 属性兼容性表 + +| 属性 | 是否继承 BaseProps | 动态更新支持 | 性能影响等级 | +| ------------- | ------------------ | ------------ | ------------ | +| `text` | 否 | ✔️ | 中 | +| `fillStyle` | 否 | ✔️ | 中 | +| `strokeStyle` | 否 | ✔️ | 中 | +| `font` | 否 | ✔️ | 高 | +| `strokeWidth` | 否 | ✔️ | 低 | + +--- + +## 注意事项 + +1. 如果需要显示多行文本,考虑使用 [TextContent](../user-client-modules/TextContent.md) +2. 考虑到浏览器兼容性,不建议在颜色中填写一些新标准的语法,例如 `rgb(0.3, 0.6, 0.8 / 0.6)` `#rgba` 等 diff --git a/docs/api/motajs-render-vue/标签 winskin.md b/docs/api/motajs-render-vue/标签 winskin.md new file mode 100644 index 0000000..7e65c53 --- /dev/null +++ b/docs/api/motajs-render-vue/标签 winskin.md @@ -0,0 +1,117 @@ +# winskin 标签 API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +--- + +```mermaid +graph LR + WinskinProps --> BaseProps + + click BaseProps "./BaseProps" +``` + +--- + +## 接口定义 + +```typescript +interface WinskinProps extends BaseProps { + /** + * 窗口皮肤图片资源ID + */ + image: ImageIds; + + /** + * 边框粗细 + * - 设置为 32 时表示原始大小(16 像素宽度),默认为 32 + * - 设为0时仅显示中心内容区 + */ + borderSize?: number; +} +``` + +--- + +## 核心功能演示 + +### 示例 1:基础窗口皮肤 + +```tsx +// 使用默认边框(borderSize=32) + + + + + +``` + +--- + +### 示例 2:不同边框粗细对比 + +```tsx +// 细边框(borderSize=16) +; + +// 粗边框(borderSize=32) +; + +// 无边框(borderSize=0) +; +``` + +**效果差异**: + +- 细边框:内容区域更大,适合信息密集场景 +- 粗边框:装饰性更强,适合标题窗口 +- 无边框:仅保留中心纹理,适合全屏背景 + +--- + +## 九宫格原理示意图 + +```typescript +/* + +-------------------+ + | 1 top 2 | ← borderSize + | | + | | +left | 4 center 3 | right + | | + | | + | 5 bottom 6 | + +-------------------+ +*/ +``` + +--- + +## 高级用法示例 + +### 动态边框动画 + +```tsx +import { hyper } from 'mutate-animate'; +import { transitioned } from '@user/client-modules'; +import { onTick } from '@motajs/render-vue'; + +const border = transitioned(16, 1000, hyper('sin', 'in-out')); +onTick(() => { + if (border.value === 16) border.set(32); + if (border.value === 32) border.set(16) +}) + + + + +; +``` diff --git a/docs/api/motajs-render/index.md b/docs/api/motajs-render/index.md new file mode 100644 index 0000000..0bb0fed --- /dev/null +++ b/docs/api/motajs-render/index.md @@ -0,0 +1,16 @@ +# @motajs/render + +此模块包含如下模块的内容,可以直接引用: + +- [@motajs/render-core](../motajs-render-core/) +- [@motajs/render-elements](../motajs-render-elements/) +- [@motajs/render-style](../motajs-render-style/) +- [@motajs/render-vue](../motajs-render-vue/) + +引入示例: + +```ts +import { Container } from '@motajs/render'; +// 二者等价,不需要单独使用一个量来接收,注意与 @motajs/client 引入方式区分 +import { Container } from '@motajs/render-core'; +``` diff --git a/docs/api/motajs-system-action/Hotkey.md b/docs/api/motajs-system-action/Hotkey.md new file mode 100644 index 0000000..60f600d --- /dev/null +++ b/docs/api/motajs-system-action/Hotkey.md @@ -0,0 +1,311 @@ +# Hotkey API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +```mermaid +graph LR + Hotkey --> EventEmitter + + click EventEmitter "https://nodejs.org/api/events.html#class-eventemitter" +``` + +## 类描述 + +`Hotkey` 是按键系统的核心类,用于管理按键绑定、辅助键状态、按键分组及事件触发逻辑。继承自 `EventEmitter`,支持自定义事件监听。主要用于实现复杂的快捷键系统。 + +--- + +## 属性说明 + +| 属性名 | 类型 | 描述 | +| -------------- | ---------------------------- | -------------------------------------------- | +| `id` | `string` | 控制器的唯一标识符 | +| `name` | `string` | 控制器的显示名称 | +| `data` | `Record` | 存储所有已注册的按键配置 | +| `keyMap` | `Map` | 按键代码到配置的映射(支持多键绑定同一操作) | +| `enabled` | `boolean` | 当前控制器是否启用(默认 `false`) | +| `list`(静态) | `Hotkey[]` | 静态属性,存储所有已创建的控制器实例 | + +--- + +## 构造方法 + +```typescript +function constructor(id: string, name: string): Hotkey; +``` + +- **参数** + - `id`: 控制器的唯一标识符 + - `name`: 控制器的显示名称 + +**示例** + +```typescript +const editorHotkey = new Hotkey('editor', '编辑器快捷键'); +``` + +--- + +## 方法说明 + +### `register` + +```typescript +function register(data: RegisterHotkeyData): this; +``` + +注册一个按键配置。 + +- **参数** + ```typescript + interface RegisterHotkeyData { + id: string; // 按键唯一标识(可含数字后缀,如 "copy_1") + name: string; // 显示名称(如 "复制") + defaults: KeyCode; // 默认按键代码 + ctrl?: boolean; // 是否默认需要 Ctrl 辅助键 + shift?: boolean; // 是否默认需要 Shift 辅助键 + alt?: boolean; // 是否默认需要 Alt 辅助键 + } + ``` + +**示例** + +```typescript +editorHotkey.register({ + id: 'copy', + name: '复制', + defaults: KeyCode.KeyC, + ctrl: true +}); +``` + +--- + +### `realize` + +```typescript +function realize(id: string, func: HotkeyFunc, config?: HotkeyEmitConfig): this; +``` + +为按键绑定触发逻辑。 + +- **参数** + - `id`: 目标按键 ID(无需后缀) + - `func`: 触发时执行的函数 + - `config`: 触发类型配置(节流/超时等) + +**示例** + +```typescript +editorHotkey.realize( + 'copy', + (id, key, ev) => { + console.log('执行复制操作'); + }, + { type: 'down-throttle', throttle: 500 } +); +``` + +--- + +### `group` + +```typescript +function group(id: string, name: string, keys?: RegisterHotkeyData[]): this; +``` + +创建按键分组,后续注册的按键自动加入该组。 + +- **参数** + - `id`: 分组唯一标识 + - `name`: 分组显示名称 + - `keys`: 可选,预注册的按键列表 + +--- + +### `set` + +```typescript +function set(id: string, key: KeyCode, assist: number, emit?: boolean): void; +``` + +动态修改按键绑定。 + +- **参数** + - `id`: 目标按键 ID + - `key`: 新按键代码 + - `assist`: 辅助键状态(二进制位:Ctrl=1<<0, Shift=1<<1, Alt=1<<2) + - `emit`: 是否触发 `set` 事件(默认 `true`) + +--- + +### `when` + +```typescript +function when(fn: () => boolean): this; +``` + +为当前作用域的按键绑定添加触发条件。 + +- **参数** + - `fn`: 条件函数,返回 `true` 时允许触发按键逻辑 + +**示例** + +```typescript +// 仅在游戏处于运行状态时允许触发 +controller.when(() => gameState === 'running'); +``` + +--- + +### `enable` + +```typescript +function enable(): void; +``` + +### `disable` + +```typescript +function disable(): void; +``` + +启用/禁用整个按键控制器(禁用后所有按键事件将被忽略)。 + +**示例** + +```typescript +// 暂停游戏时禁用按键 +controller.disable(); +``` + +--- + +### `use` + +```typescript +function use(symbol: symbol): void; +``` + +切换当前作用域,后续 `realize` 方法绑定的逻辑将关联到该作用域。 + +- **参数** + - `symbol`: 唯一作用域标识符 + +--- + +### `dispose` + +```typescript +function dispose(symbol?: symbol): void; +``` + +释放指定作用域及其绑定的所有按键逻辑。 + +- **参数** + - `symbol`(可选): 要释放的作用域(默认释放当前作用域) + +**示例** + +```typescript +const scope = Symbol(); +controller.use(scope); +// ...绑定操作... +controller.dispose(scope); // 释放该作用域 +``` + +--- + +### `emitKey` + +```typescript +function emitKey( + key: KeyCode, + assist: number, + type: KeyEventType, + ev: KeyboardEvent +): boolean; +``` + +手动触发按键事件(可用于模拟按键操作)。 + +- **参数** + - `key`: 按键代码 + - `assist`: 辅助键状态(二进制位:Ctrl=1<<0, Shift=1<<1, Alt=1<<2) + - `type`: 事件类型(`'up'` 或 `'down'`) + - `ev`: 原始键盘事件对象 +- **返回值** + `true` 表示事件被成功处理,`false` 表示无匹配逻辑 + +**示例** + +```typescript +// 模拟触发 Ctrl+S 保存操作 +controller.emitKey( + KeyCode.KeyS, + 1 << 0, // Ctrl 激活 + 'down', + new KeyboardEvent('keydown') +); +``` + +--- + +## 静态方法说明 + +### `Hotkey.get` + +```typescript +function get(id: string): Hotkey | undefined; +``` + +**静态方法**:根据 ID 获取控制器实例。 + +--- + +## 事件说明 + +| 事件名 | 参数类型 | 触发时机 | +| --------- | --------------------------------------------------- | ---------------- | +| `set` | `[id: string, key: KeyCode, assist: number]` | 按键绑定被修改时 | +| `emit` | `[key: KeyCode, assist: number, type: KeyEmitType]` | 按键被触发时 | +| `press` | `[key: KeyCode]` | 按键被按下时 | +| `release` | `[key: KeyCode]` | 按键被释放时 | + +**事件监听示例** + +```typescript +editorHotkey.on('emit', (key, assist) => { + console.log(`按键 ${KeyCode[key]} 触发,辅助键状态:${assist}`); +}); +``` + +--- + +## 总使用示例 + +::: code-group + +```typescript [注册] +import { gameKey } from '@motajs/system-action'; + +gameKey.register({ + id: 'jump', + name: '跳跃', + defaults: KeyCode.Space +}); +``` + +```typescript [实现] +import { useKey } from '@motajs/render-vue'; + +const [gameKey] = useKey(); + +// 绑定跳跃逻辑 +gameKey.realize('jump', () => { + player.jump(); +}); +``` + +::: diff --git a/docs/api/motajs-system-action/Keyboard.md b/docs/api/motajs-system-action/Keyboard.md new file mode 100644 index 0000000..f3929fa --- /dev/null +++ b/docs/api/motajs-system-action/Keyboard.md @@ -0,0 +1,221 @@ +# Keyboard API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +```mermaid +graph LR + Keyboard --> EventEmitter +``` + +## 类描述 + +`Keyboard` 是虚拟键盘的核心类,用于管理动态按键布局、处理按键事件及辅助键状态。继承自 `EventEmitter`,支持自定义事件监听。 + +--- + +## 属性说明 + +| 属性名 | 类型 | 描述 | +| -------------- | ---------------- | ----------------------------------------------------------- | +| `id` | `string` | 键盘的唯一标识符 | +| `keys` | `KeyboardItem[]` | 当前键盘包含的按键列表(响应式数组) | +| `assist` | `number` | 辅助键状态(二进制位表示:Ctrl=1<<0, Shift=1<<1, Alt=1<<2) | +| `fontSize` | `number` | 按键文本字体大小(默认 18) | +| `list`(静态) | `Keyboard[]` | 静态属性,存储所有已创建的键盘实例 | + +**KeyboardItem 结构**: + +```typescript +interface KeyboardItem { + key: KeyCode; // 按键代码 + text?: string; // 显示文本(可选) + x: number; // X 坐标 + y: number; // Y 坐标 + width: number; // 宽度 + height: number; // 高度 +} +``` + +--- + +## 构造方法 + +```typescript +function constructor(id: string): Keyboard; +``` + +- **参数** + - `id`: 键盘的唯一标识符 + +**示例** + +```typescript +const numpad = new Keyboard('numpad'); +``` + +--- + +## 方法说明 + +### `add` + +```typescript +function add(item: KeyboardItem): Keyboard; +``` + +### `remove` + +```typescript +function remove(item: KeyboardItem): Keyboard; +``` + +添加/移除按键,返回当前实例以便链式调用。 + +**示例** + +```typescript +// 添加数字键 1 +numpad.add({ + key: KeyCode.Digit1, + text: '1', + x: 0, + y: 0, + width: 60, + height: 60 +}); + +// 移除按键 +numpad.remove(existingKey); +``` + +--- + +### `extend` + +```typescript +function extend( + keyboard: Keyboard, + offsetX?: number, + offsetY?: number +): Keyboard; +``` + +继承其他键盘的按键布局并添加偏移量。 + +**示例** + +```typescript +const extendedKB = new Keyboard('extended'); +extendedKB.extend(numpad, 100, 0); // 向右偏移 100px +``` + +--- + +### `emitKey` + +```typescript +function emitKey(key: KeyboardItem, index: number): void; +``` + +模拟触发按键事件(自动处理辅助键状态)。 + +--- + +### `createScope` + +```typescript +function createScope(): symbol; +``` + +### `disposeScope` + +```typescript +function disposeScope(): void; +``` + +管理事件监听作用域: + +- `createScope`: 创建新作用域(返回唯一标识符) +- `disposeScope`: 释放当前作用域 + +--- + +### `withAssist` + +```typescript +function withAssist(assist: number): symbol; +``` + +创建预设辅助键状态的作用域(如 `Ctrl+Shift`)。 + +--- + +### `Keyboard.get` + +```typescript +function get(id: string): Keyboard | undefined; +``` + +**静态方法**:根据 ID 获取键盘实例。 + +--- + +## 事件说明 + +| 事件名 | 参数类型 | 触发时机 | +| -------------- | ------------------------- | ------------------ | +| `add` | `KeyboardItem` | 新增按键时 | +| `remove` | `KeyboardItem` | 移除按键时 | +| `extend` | `Keyboard` | 继承其他键盘布局时 | +| `emit` | `item, assist, index, ev` | 触发按键时 | +| `scopeCreate` | `symbol` | 创建作用域时 | +| `scopeDispose` | `symbol` | 释放作用域时 | + +**事件监听示例** + +```typescript +numpad.on('emit', (item, assist) => { + console.log(`按键 ${item.key} 触发,辅助键状态:${assist}`); +}); +``` + +--- + +## 总使用示例 + +```typescript +import { KeyCode } from '@motajs/client-base'; +import { Keyboard } from '@motajs/system-action'; + +// 创建数字键盘 +const numpad = new Keyboard('numpad'); + +// 添加基础按键 +numpad + .add({ key: KeyCode.Digit1, x: 0, y: 0, width: 60, height: 60 }) + .add({ key: KeyCode.Digit2, x: 60, y: 0, width: 60, height: 60 }); + +// 添加功能键(带辅助状态) +const ctrlScope = numpad.withAssist(1 << 0); // Ctrl 激活 +numpad.add({ + key: KeyCode.KeyC, + text: '复制', + x: 120, + y: 0, + width: 120, + height: 60 +}); + +// 监听复制键 +numpad.on('emit', item => { + if (item.key === KeyCode.KeyC) { + console.log('执行复制操作'); + } +}); + +// 触发按键 +numpad.emitKey(numpad.keys[0], 0); // 模拟按下数字 1 + +// 获取键盘实例 +const foundKB = Keyboard.get('numpad'); +``` diff --git a/docs/api/motajs-system-action/index.md b/docs/api/motajs-system-action/index.md new file mode 100644 index 0000000..970f853 --- /dev/null +++ b/docs/api/motajs-system-action/index.md @@ -0,0 +1,3 @@ +# @motajs/system-action + +目录: diff --git a/docs/api/motajs-system-ui/GameUI.md b/docs/api/motajs-system-ui/GameUI.md new file mode 100644 index 0000000..c8dae21 --- /dev/null +++ b/docs/api/motajs-system-ui/GameUI.md @@ -0,0 +1,97 @@ +# GameUI API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +```mermaid +graph LR + GameUI --> IGameUI +``` + +_实现 `IGameUI` 接口_ + +## 接口描述 + +`IGameUI` 是 UI 系统的核心接口,定义了 UI 实例的基础结构和静态操作方法。 + +--- + +## 属性说明 + +| 属性名 | 类型 | 描述 | +| -------------- | ---------------------------------- | ------------------------------------------------------------------- | +| `name` | `string` | 只读,UI 的唯一标识名称 | +| `component` | `C extends UIComponent` | 只读,关联的 Vue 组件实例 | +| `list`(静态) | `Map>` | 静态属性,存储所有已注册的 UI 实例,键为 `name`,值为 `GameUI` 实例 | + +--- + +## 构造方法 + +### `consturctor` + +```typescript +function constructor(name: string, component: C): GameUI; +``` + +- **参数** + - `name`: UI 的唯一标识名称 + - `component`: 关联的 Vue 组件实例 + +**示例** + +```typescript +import { defineComponent } from 'vue'; + +export const MyComponent = defineComponent(...); + +// 创建 UI 实例并自动注册到静态列表 +export const MyUI = new GameUI('my-ui', MyComponent); +``` + +--- + +## 方法说明 + +### `GameUI.get` + +```typescript +function get(id: string): GameUI | null; +``` + +**静态方法**:根据 UI 名称从静态列表获取实例。 + +- **参数** + - `id`: UI 的唯一标识名称 +- **返回值** + 匹配的 `GameUI` 实例,未找到时返回 `null` + +**示例** + +```typescript +// 获取名为 "home" 的 UI 实例 +const ui = GameUI.get('my-ui'); +if (ui) { + console.log(ui.component); // 输出关联的 Vue 组件 +} +``` + +--- + +## 总使用示例 + +```tsx +import { defineComponent } from 'vue'; +import { GameUI } from '@motajs/system-ui'; + +// 定义组件 +export const MyCom = defineComponent(() => { + return () => ( + + + + ); +}); + +// 定义 UI 实例 +export const MyUI = new GameUI('my-ui', MyCom); +``` diff --git a/docs/api/motajs-system-ui/UIController.md b/docs/api/motajs-system-ui/UIController.md new file mode 100644 index 0000000..e781db1 --- /dev/null +++ b/docs/api/motajs-system-ui/UIController.md @@ -0,0 +1,284 @@ +# UIController API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +```mermaid +graph LR + UIController --> EventEmitter + + click EventEmitter "https://nodejs.org/api/events.html#class-eventemitter" +``` + +## 类描述 + +`UIController` 是 UI 控制系统的核心类,负责管理 UI 实例的显示栈、背景控制以及多种显示模式。继承自 `EventEmitter`,支持事件监听。想要编写 UI 请参考[深度指南](../../guide/ui.md)。 + +--- + +## 属性说明 + +| 属性名 | 类型 | 描述 | +| --------------------- | ---------------------------- | --------------------------------------------------------------- | +| `stack` | `IUIInstance[]` (响应式数组) | 当前管理的 UI 实例栈 | +| `mode` | `UIMode` | UI 显示模式,默认为 `LastOnlyStack` | +| `background` | `IGameUI` | 背景 UI 的配置实例 | +| `backIns` | `ShallowRef` | 背景 UI 实例的响应式引用 | +| `showBack` | `ComputedRef` | 当前是否显示背景(用户设置与系统状态共同决定) | +| `active`(只读) | `boolean` | 系统是否显示背景 UI(等价于 `sysShowBack.value`) | +| `controllers`(静态) | `Map` | 静态属性,存储所有已创建的控制器实例,键为 `id`,值为控制器实例 | + +--- + +## 构造方法 + +### `constructor` + +```typescript +function constructor(id: string): UIController; +``` + +- **参数** + - `id`: 控制器的唯一标识符(若重复会触发警告日志) + +**示例** + +```typescript +const mainController = new UIController('main'); +``` + +--- + +## 方法说明 + +### `render` + +```typescript +function render(): VNode; +``` + +渲染 UI 容器组件(用于 Vue 挂载)。 + +**示例** + +```tsx +import { defineComponent } from 'vue'; +import { UIController } from '@motajs/system-ui'; + +export const myController = new UIController('my-controller'); + +export const MyCom = defineComponent(() => { + return () => {myController.render()}; +}); +``` + +--- + +### `setBackground` + +```typescript +function setBackground( + back: IGameUI, + vBind: UIProps +): void; +``` + +设置背景 UI 并初始化其实例。 + +- **参数** + - `back`: 背景 UI 配置实例 + - `vBind`: 传递给背景组件的 Props 对象 + +**示例** + +```typescript +import { BackgroundUI } from './background'; + +// 显示背景组件,并传入参数 theme='dark' +mainController.setBackground(BackgroundUI, { theme: 'dark' }); +``` + +--- + +### `hideBackground` + +```typescript +function hideBackground(): void; +``` + +隐藏背景 UI (不影响系统状态)。 + +--- + +### `showBackground` + +```typescript +function showBackground(): void; +``` + +显示背景 UI(不影响系统状态)。 + +--- + +### `keep` + +```typescript +function keep(): IKeepController; +``` + +维持背景显示(防闪烁),返回控制器对象: + +```typescript +interface IKeepController { + safelyUnload(): void; // 安全卸载(仅在栈为空时关闭) + unload(): void; // 强制立即卸载 +} +``` + +**示例** + +```typescript +const keeper = mainController.keep(); +// 执行某些操作后... +keeper.safelyUnload(); +``` + +--- + +### `open` + +```typescript +function open( + ui: IGameUI, + vBind: UIProps, + alwaysShow?: boolean +): IUIInstance; +``` + +打开一个新 UI 实例并加入栈中。 + +- **参数** + - `ui`: UI 配置实例 + - `vBind`: 组件 Props + - `alwaysShow`: 是否强制显示(默认 `false`) +- **返回值** + 打开的 UI 实例,可以用于关闭等操作 + +**示例** + +```typescript +import { MyUI } from './myUI'; + +const instance = mainController.open(MyUI, { param: 80 }); +``` + +--- + +### `close` + +```typescript +function close(ui: IUIInstance): void; +``` + +关闭指定 UI 实例(根据当前模式可能影响其他实例)。 + +--- + +### `closeAll` + +```typescript +function closeAll(ui?: IGameUI): void; +``` + +关闭所有或指定类型的所有 UI 实例。 + +- **参数** + - `ui`(可选): 指定要关闭的 UI 类型,不填时表示关闭所有 UI + +--- + +### `lastOnly` + +```typescript +function lastOnly(stack?: boolean): void; +``` + +切换显示模式:仅显示最后一个 UI(可设置为栈模式) + +--- + +### `showAll` + +```typescript +function showAll(stack?: boolean): void; +``` + +切换显示模式:显示所有非隐藏 UI(可设置为栈模式) + +--- + +### `showCustom` + +```typescript +function showCustom(config: IUICustomConfig): void; +``` + +切换显示模式:使用自定义模式(需实现 `IUICustomConfig`),参考[指南](../../guide/ui-system.md#自定义显示模式) + +--- + +### `UIController.getController` + +```typescript +function getController(id: string): UIController | null; +``` + +**静态方法**:根据 ID 获取控制器实例。 + +**示例** + +```typescript +const ctrl = UIController.getController('main'); +``` + +--- + +## 事件说明 + +| 事件名 | 参数类型 | 触发时机 | +| ------- | ------------------------------- | ------------------ | +| `open` | `ui: IGameUI, ins: IUIInstance` | 新 UI 实例被打开时 | +| `close` | `ins: IUIInstance` | UI 实例被关闭时 | + +**事件监听示例** + +```typescript +mainController.on('open', (ui, ins) => { + console.log(`Opened UI: ${ui.name}`); +}); +``` + +--- + +## 总使用示例 + +```typescript +import { BackgroundUI, DialogUI } from './myUI'; +import { mainController } from '@user/client-modules'; + +// 事件监听 +mainController.on('close', ins => { + console.log('UI closed:', ins.ui.name); +}); + +// 设置背景 +mainController.setBackground(BackgroundUI, { color: '#333' }); + +// 打开 UI +const dialogIns = mainController.open(DialogUI, { title: '提示' }); + +// 切换显示模式,仅显示最后一个,启用栈模式 +mainController.lastOnly(true); + +// 关闭 UI +mainController.close(dialogIns); +``` diff --git a/docs/api/motajs-system-ui/UIInstance.md b/docs/api/motajs-system-ui/UIInstance.md new file mode 100644 index 0000000..53c7890 --- /dev/null +++ b/docs/api/motajs-system-ui/UIInstance.md @@ -0,0 +1,121 @@ +# UIInstance API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +```mermaid +graph LR + UIInstance --> IUIInstance +``` + +_实现 `IUIInstance` 接口_ + +## 类描述 + +`UIInstance` 表示通过 `GameUI` 模板创建的具体 UI 实例,用于管理单个 UI 实例的状态和数据绑定。实现了 `IUIInstance` 接口。 + +--- + +## 属性说明 + +| 属性名 | 类型 | 描述 | +| ------------ | ------------ | -------------------------------------------------------- | +| `key` | `number` | 只读,实例的唯一标识(用于 Vue 的 `key` 属性) | +| `ui` | `IGameUI` | 只读,关联的 UI 配置实例(即创建该实例的 `GameUI` 模板) | +| `vBind` | `UIProps` | 只读,传递给 UI 组件的响应式 Props 对象 | +| `hidden` | `boolean` | 当前实例是否处于隐藏状态 | +| `alwaysShow` | `boolean` | 是否强制保持显示(不受显示模式影响) | + +--- + +## 构造方法 + +```typescript +function constructor( + ui: IGameUI, + vBind: UIProps, + alwaysShow: boolean = false +): UIInstance; +``` + +- **参数** + - `ui`: 关联的 `GameUI` 配置实例 + - `vBind`: 初始化的组件 Props 对象 + - `alwaysShow`: 是否强制保持显示(默认 `false`) + +**注意事项**:一般不需要手动创建 `UIInstance` 实例,请使用 [`UIController.open`](./UIController.md#open) 打开 UI 并创建实例。 + +--- + +## 方法说明 + +### `setVBind` + +```typescript +function setVBind(data: Partial>, merge?: boolean): void; +``` + +更新组件的响应式 Props。 + +- **参数** + - `data`: 需要更新的数据(部分 Props) + - `merge`: 是否与现有数据合并(默认 `true`),若为 `false` 则完全覆盖 + +**示例** + +```typescript +// 合并更新音量值 +instance.setVBind({ volume: 60 }); + +// 覆盖所有 Props +instance.setVBind({ theme: 'dark' }, false); +``` + +--- + +### `hide` + +```typescript +function hide(): void; +``` + +控制实例的显示状态(直接操作 `hidden` 属性)。 + +**示例** + +```typescript +instance.hide(); // 隐藏 UI +setTimeout(() => instance.show(), 1000); // 1 秒后显示 +``` + +--- + +### `show` + +```typescript +function show(): void; +``` + +控制实例的显示状态(直接操作 `hidden` 属性)。 + +**示例** + +```typescript +instance.show(); // 隐藏 UI +setTimeout(() => instance.show(), 1000); // 1 秒后显示 +``` + +--- + +## 总使用示例 + +```typescript +import { myController, MyUI } from './myUI'; + +const myIns = myController.open(MyUI, { title: '警告' }); + +// 动态更新 props +myIns.setVBind({ title: '错误' }); + +// 设置显示状态 +myIns.show(); +``` diff --git a/docs/api/motajs-system-ui/index.md b/docs/api/motajs-system-ui/index.md new file mode 100644 index 0000000..f84a151 --- /dev/null +++ b/docs/api/motajs-system-ui/index.md @@ -0,0 +1,3 @@ +# @motajs/system-ui + +目录: diff --git a/docs/api/motajs-system/index.md b/docs/api/motajs-system/index.md new file mode 100644 index 0000000..76a8633 --- /dev/null +++ b/docs/api/motajs-system/index.md @@ -0,0 +1,25 @@ +# @motajs/system + +包含两个模块: + +- [`@motajs/system-action`](../motajs-system-action/index.md) +- [`@motajs/system-ui`](../motajs-system-ui/index.md) + +## 引入示例 + +```ts +import { gameKey, UIController } from '@motajs/system'; + +gameKey.register(...); +const myController = new UIController('my-controller'); +``` + +等效于: + +```ts +import { gameKey } from '@motajs/system-action'; +import { UIController } from '@motajs/system-ui'; + +gameKey.register(...); +const myController = new UIController('my-controller'); +``` diff --git a/docs/api/motajs-types/index.md b/docs/api/motajs-types/index.md new file mode 100644 index 0000000..d3d5030 --- /dev/null +++ b/docs/api/motajs-types/index.md @@ -0,0 +1,3 @@ +# @motajs/types + +目录: diff --git a/docs/api/user-client-modules/AudioDecoder.md b/docs/api/user-client-modules/AudioDecoder.md new file mode 100644 index 0000000..249c8aa --- /dev/null +++ b/docs/api/user-client-modules/AudioDecoder.md @@ -0,0 +1,142 @@ +# AudioDecoder API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +--- + +## 类描述 + +音频解码系统的核心抽象类,为不同音频格式提供统一的解码接口。主要处理浏览器原生不支持音频格式的解码任务(如 iOS 平台的 Ogg 格式)。 + +--- + +## 静态成员说明 + +### `decoderMap` + +```typescript +declare const decoderMap: Map AudioDecoder>; +``` + +解码器注册表,存储格式类型与解码器类的映射关系 + +--- + +## 静态方法说明 + +### `AudioDecoder.registerDecoder` + +```typescript +function registerDecoder( + type: AudioType, + decoder: new () => AudioDecoder +): void; +``` + +注册自定义解码器到全局解码器系统 + +| 参数 | 类型 | 说明 | +| ------- | ------------------------ | -------------- | +| type | `AudioType` | 音频格式类型 | +| decoder | `new () => AudioDecoder` | 解码器构造函数 | + +--- + +### `AudioDecoder.decodeAudioData` + +```typescript +function decodeAudioData( + data: Uint8Array, + player: AudioPlayer +): Promise; +``` + +核心解码入口方法,自动选择最佳解码方案 + +| 参数 | 类型 | 说明 | +| ------ | ------------- | ---------------- | +| data | `Uint8Array` | 原始音频字节数据 | +| player | `AudioPlayer` | 音频播放器实例 | + +**处理流程**: + +1. 通过文件头检测音频类型 +2. 优先使用浏览器原生解码能力 +3. 无原生支持时查找注册的自定义解码器 +4. 返回标准 `AudioBuffer` 格式数据 + +--- + +## 抽象方法说明 + +### `abstract create` + +```typescript +function create(): Promise; +``` + +初始化解码器实例(需分配 WASM 内存等资源) + +--- + +### `abstract destroy` + +```typescript +function destroy(): void; +``` + +销毁解码器实例(需释放资源) + +--- + +### `abstract decode` + +```typescript +function decode(data: Uint8Array): Promise; +``` + +流式解码方法(分块处理) + +| 参数 | 类型 | 说明 | +| ---- | ------------ | ------------ | +| data | `Uint8Array` | 音频数据分块 | + +--- + +### `abstract decodeAll` + +```typescript +function decodeAll(data: Uint8Array): Promise; +``` + +全量解码方法(单次处理完整文件) + +--- + +### `abstract flush` + +```typescript +function flush(): Promise; +``` + +冲刷解码器缓冲区,获取残留数据 + +--- + +## 数据结构 + +### IAudioDecodeData + +```typescript +interface IAudioDecodeData { + channelData: Float32Array[]; // 各声道 PCM 数据 + samplesDecoded: number; // 已解码采样数 + sampleRate: number; // 采样率 (Hz) + errors: IAudioDecodeError[]; // 解码错误集合 +} +``` + +## 内置解码器 + +- `VorbisDecoder`: 解码 ogg vorbis 音频。 +- `OpusDecoder`: 解码 ogg opus 音频。 diff --git a/docs/api/user-client-modules/AudioEffect.md b/docs/api/user-client-modules/AudioEffect.md new file mode 100644 index 0000000..0853131 --- /dev/null +++ b/docs/api/user-client-modules/AudioEffect.md @@ -0,0 +1,207 @@ +# AudioEffect API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +--- + +## 类描述 + +音频处理管道的核心抽象类,为构建音频效果链提供基础框架。所有效果器通过输入/输出节点串联,形成可定制的音频处理流水线。 + +--- + +## 核心架构 + +音频播放流程: + +```mermaid +graph LR + Source --> Effect1 + Effect1 --> Effect2[...] + Effect2 --> GainNode + GainNode --> Destination +``` + +--- + +## 抽象成员说明 + +| 成员 | 类型 | 说明 | +| -------- | ----------- | -------------------------- | +| `input` | `AudioNode` | 效果器输入节点(必须实现) | +| `output` | `AudioNode` | 效果器输出节点(必须实现) | + +--- + +## 核心方法说明 + +### `connect` + +```typescript +function connect(target: IAudioInput, output?: number, input?: number): void; +``` + +连接至下游音频节点 + +| 参数 | 类型 | 说明 | +| ------ | ------------- | -------------------------- | +| target | `IAudioInput` | 目标效果器/节点 | +| output | `number` | 当前效果器输出通道(可选) | +| input | `number` | 目标效果器输入通道(可选) | + +--- + +### `disconnect` + +```typescript +function disconnect( + target?: IAudioInput, + output?: number, + input?: number +): void; +``` + +断开与下游节点的连接 + +--- + +### `abstract start` + +```typescript +function start(): void; +``` + +效果器激活时调用(可用于初始化参数) + +--- + +### `abstract end` + +```typescript +function end(): void; +``` + +效果器停用时调用(可用于资源回收) + +--- + +## 自定义效果器示例 + +### 混响效果器实现 + +```typescript +export class ReverbEffect extends AudioEffect { + private convolver: ConvolverNode; + private dryGain: GainNode; + private wetGain: GainNode; + + constructor(ac: AudioContext) { + super(ac); + + // 创建节点网络 + this.dryGain = ac.createGain(); + this.wetGain = ac.createGain(); + this.convolver = ac.createConvolver(); + + // 定义输入输出 + this.input = this.dryGain; + this.output = this.ac.createGain(); + + // 构建处理链 + this.dryGain.connect(this.output); + this.dryGain.connect(this.convolver); + this.convolver.connect(this.wetGain); + this.wetGain.connect(this.output); + } + + /** 设置混响强度 */ + setMix(value: number) { + this.dryGain.gain.value = 1 - value; + this.wetGain.gain.value = value; + } + + /** 加载脉冲响应 */ + async loadImpulse(url: string) { + const response = await fetch(url); + const buffer = await this.ac.decodeAudioData( + await response.arrayBuffer() + ); + this.convolver.buffer = buffer; + } + + start() { + this.output.gain.value = 1; + } + + end() { + this.output.gain.value = 0; + } +} +``` + +--- + +## 内置效果器说明 + +### StereoEffect(立体声控制) + +```mermaid +graph LR + Input --> Panner[PannerNode] + Panner --> Output +``` + +- 控制声相/3D 空间定位 +- 支持设置声音方位和位置 + +### VolumeEffect(音量控制) + +```mermaid +graph LR + Input --> Gain[GainNode] + Gain --> Output +``` + +- 全局音量调节 +- 支持实时音量渐变 + +### ChannelVolumeEffect(多声道控制) + +```mermaid +graph LR + Input --> Splitter[ChannelSplitter] + Splitter --> Gain1 + Splitter --> Gain2 + Gain1 --> Merger + Gain2 --> Merger + Merger --> Output +``` + +- 6 声道独立音量控制 +- 支持环绕声场调节 + +### DelayEffect(延迟效果) + +```mermaid +graph LR + Input --> Delay[DelayNode] + Delay --> Output +``` + +- 基础延迟效果 +- 精确到采样级的延迟控制 + +### EchoEffect(回声效果) + +```mermaid +graph LR + Input --> Gain + Gain --> Delay + Delay --> Gain[反馈循环] + Gain --> Output +``` + +- 带反馈的延迟效果 +- 自动渐弱回声处理 + +--- diff --git a/docs/api/user-client-modules/AudioPlayer.md b/docs/api/user-client-modules/AudioPlayer.md new file mode 100644 index 0000000..0a5125b --- /dev/null +++ b/docs/api/user-client-modules/AudioPlayer.md @@ -0,0 +1,172 @@ +# AudioPlayer API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +--- + +## 类描述 + +音频系统的核心控制器,负责管理音频上下文、路由系统、效果器工厂和全局音频参数。支持多音轨管理和 3D 音频空间化配置。 + +```mermaid +graph LR + AudioPlayer --> EventEmitter + + click EventEmitter "https://nodejs.org/api/events.html#class-eventemitter" +``` + +--- + +## 核心架构 + +```mermaid +graph TD + Player[AudioPlayer] --> Sources[音频源工厂] + Player --> Effects[效果器工厂] + Player --> Routes[路由系统] + Player --> Listener[3D 听者配置] +``` + +--- + +## 属性说明 + +| 属性名 | 类型 | 说明 | +| ------------- | ------------------------- | ------------------------ | +| `ac` | `AudioContext` | Web Audio API 上下文实例 | +| `audioRoutes` | `Map` | 已注册的音频路由表 | +| `gain` | `GainNode` | 全局音量控制节点 | + +--- + +## 方法说明 + +此处暂时只列出方法的简易说明。方法理解难度不高,如果需要可以自行查看代码以及其相关注释来查看如何使用。 + +### 音频源工厂方法 + +| 方法名 | 返回值 | 说明 | +| ----------------------- | -------------------- | ----------------------------- | +| `createSource(Source)` | `AudioSource` | 创建自定义音频源 | +| `createStreamSource()` | `AudioStreamSource` | 创建流式音频源(直播/长音频) | +| `createElementSource()` | `AudioElementSource` | 基于 HTML5 Audio 元素的音源 | +| `createBufferSource()` | `AudioBufferSource` | 基于 AudioBuffer 的静态音源 | + +### 效果器工厂方法 + +| 方法名 | 返回值 | 说明 | +| ----------------------------- | --------------------- | ---------------------------- | +| `createEffect(Effect)` | `AudioEffect` | 创建自定义效果器 | +| `createVolumeEffect()` | `VolumeEffect` | 全局音量控制器 | +| `createStereoEffect()` | `StereoEffect` | 立体声场控制器 | +| `createChannelVolumeEffect()` | `ChannelVolumeEffect` | 多声道独立音量控制(6 声道) | +| `createDelayEffect()` | `DelayEffect` | 精确延迟效果器 | +| `createEchoEffect()` | `EchoEffect` | 回声效果器(带反馈循环) | + +### 路由管理方法 + +| 方法名 | 参数 | 说明 | +| --------------------- | -------------------- | -------------- | +| `createRoute(source)` | `AudioSource` | 创建新播放路由 | +| `addRoute(id, route)` | `string, AudioRoute` | 注册命名路由 | +| `getRoute(id)` | `string` | 获取已注册路由 | +| `removeRoute(id)` | `string` | 移除指定路由 | + +### 全局控制方法 + +| 方法名 | 参数 | 说明 | +| ------------------------------- | ------------------------ | -------------------- | +| `setVolume(volume)` | `number` (0.0-1.0) | 设置全局音量 | +| `getVolume()` | - | 获取当前全局音量 | +| `setListenerPosition(x,y,z)` | `number, number, number` | 设置听者 3D 空间坐标 | +| `setListenerOrientation(x,y,z)` | `number, number, number` | 设置听者朝向 | +| `setListenerUp(x,y,z)` | `number, number, number` | 设置听者头顶朝向 | + +--- + +## 使用示例 + +### 基础音乐播放 + +```typescript +import { audioPlayer } from '@user/client-modules'; + +// 创建音频源(以音频缓冲为例) +const bgmSource = audioPlayer.createBufferSource(); + +// 创建播放路由 +const bgmRoute = audioPlayer.createRoute(bgmSource); + +// 添加效果链 +bgmRoute.addEffect([ + audioPlayer.createStereoEffect(), + audioPlayer.createVolumeEffect() +]); + +// 播放控制 +audioPlayer.play('bgm'); +audioPlayer.pause('bgm'); +``` + +### 3D 环境音效 + +```typescript +import { audioPlayer } from '@user/client-modules'; + +// 配置3D听者 +audioPlayer.setListenerPosition(0, 0, 0); // 听者在原点 +audioPlayer.setListenerOrientation(0, 0, -1); // 面朝屏幕内 + +// 创建环境音源 +const ambientSource = audioPlayer.createBufferSource(); +await ambientSource.setBuffer(/* 这里填写音频缓冲 */); + +// 配置3D音效路由 +const ambientRoute = audioPlayer.createRoute(ambientSource); +const stereo = audioPlayer.createStereoEffect(); +stereo.setPosition(5, 2, -3); // 音源位于右前方高处 +ambientRoute.addEffect(stereo); + +// 循环播放 +ambientRoute.setLoop(true); +audioPlayer.addRoute('ambient', ambientRoute); +audioPlayer.play('ambient'); +``` + +--- + +## 生命周期管理 + +```mermaid +sequenceDiagram + participant User + participant AudioPlayer + participant AudioContext + + User->>AudioPlayer: new AudioPlayer() + AudioPlayer->>AudioContext: 创建音频上下文 + User->>AudioPlayer: createRoute() + AudioPlayer->>AudioRoute: 实例化路由 + User->>AudioPlayer: play() + AudioPlayer->>AudioContext: 启动音频时钟 + loop 播放周期 + AudioPlayer->>AudioRoute: 更新状态 + end + User->>AudioPlayer: stop() + AudioPlayer->>AudioRoute: 释放资源 + AudioPlayer->>AudioContext: 关闭上下文 +``` + +--- + +## 注意事项 + +1. **空间音频配置** + 3D 效果需统一坐标系: + + ```txt + (0,0,0) 屏幕中心 + X+ → 右 + Y+ ↑ 上 + Z+ ⊙ 朝向用户 + ``` diff --git a/docs/api/user-client-modules/AudioRoute.md b/docs/api/user-client-modules/AudioRoute.md new file mode 100644 index 0000000..681f00f --- /dev/null +++ b/docs/api/user-client-modules/AudioRoute.md @@ -0,0 +1,221 @@ +# AudioRoute 音频播放路由 API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +--- + +## 类描述 + +音频播放控制的核心类,负责管理音频源与效果器的连接关系,协调播放状态转换,并处理音频管线生命周期。 + +```mermaid +graph LR + AudioRoute --> EventEmitter + + click EventEmitter "https://nodejs.org/api/events.html#class-eventemitter" +``` + +--- + +## 属性说明 + +| 属性名 | 类型 | 说明 | +| ------------- | ------------------ | ------------------------------------------------ | +| `output` | `AudioNode` | 最终输出节点(继承自 IAudioOutput) | +| `effectRoute` | `AudioEffect[]` | 效果器链数组(按顺序存储已连接的效果器实例) | +| `endTime` | `number` | 淡出过渡时长(单位:秒),默认 0 | +| `status` | `AudioStatus` | 当前播放状态(见下方枚举定义) | +| `duration` | `number` (getter) | 音频总时长(单位:秒) | +| `currentTime` | `number` (get/set) | 当前播放进度(单位:秒),设置时会触发 seek 操作 | + +--- + +### AudioStatus 枚举 + +```typescript +enum AudioStatus { + Playing, // 正在播放 + Pausing, // 淡出暂停过程中 + Paused, // 已暂停 + Stoping, // 淡出停止过程中 + Stoped // 已停止 +} +``` + +--- + +## 方法说明 + +### `setEndTime` + +```typescript +function setEndTime(time: number): void; +``` + +设置淡出过渡时长 + +| 参数 | 类型 | 说明 | +| ---- | -------- | ------------------------ | +| time | `number` | 淡出动画时长(单位:秒) | + +--- + +### `onStart` + +```typescript +function onStart(fn?: (route: AudioRoute) => void): void; +``` + +注册播放开始钩子函数 + +| 参数 | 类型 | 说明 | +| ---- | ----------------- | ------------------------ | +| `fn` | `(route) => void` | 播放开始时触发的回调函数 | + +--- + +### `onEnd` + +```typescript +function onEnd(fn?: (time: number, route: AudioRoute) => void): void; +``` + +注册播放结束钩子函数 + +| 参数 | 类型 | 说明 | +| ---- | --------------------------- | --------------------------------------------- | +| `fn` | `(duration, route) => void` | 淡出阶段开始时触发的回调,duration 为淡出时长 | + +--- + +### `play` + +```typescript +function play(when?: number = 0): Promise; +``` + +启动/恢复音频播放 + +| 参数 | 类型 | 说明 | +| ------ | -------- | -------------------------------------- | +| `when` | `number` | 基于 AudioContext 时间的启动时刻(秒) | + +--- + +### `pause` + +```typescript +function pause(): Promise; +``` + +触发暂停流程(执行淡出过渡) + +--- + +### `resume` + +```typescript +function resume(): void; +``` + +从暂停状态恢复播放(执行淡入过渡) + +--- + +### `stop` + +```typescript +function stop(): Promise; +``` + +完全停止播放并释放资源 + +--- + +### `addEffect` + +```typescript +function addEffect(effect: AudioEffect | AudioEffect[], index?: number): void; +``` + +添加效果器到处理链 + +| 参数 | 类型 | 说明 | +| -------- | ------------------ | ------------------------------ | +| `effect` | `AudioEffect`/数组 | 要添加的效果器实例 | +| `index` | `number` (可选) | 插入位置,负数表示从末尾倒计数 | + +--- + +### `removeEffect` + +```typescript +function removeEffect(effect: AudioEffect): void; +``` + +从处理链移除效果器 + +| 参数 | 类型 | 说明 | +| -------- | ------------- | ------------------ | +| `effect` | `AudioEffect` | 要移除的效果器实例 | + +--- + +## 事件说明 + +| 事件名 | 参数 | 触发时机 | +| -------------- | ---- | ------------------ | +| `updateEffect` | - | 效果器链发生变更时 | +| `play` | - | 开始/恢复播放时 | +| `stop` | - | 完全停止播放后 | +| `pause` | - | 进入暂停状态后 | +| `resume` | - | 从暂停状态恢复时 | + +--- + +## 总使用示例 + +```typescript +import { audioPlayer } from '@user/client-modules'; + +// 创建音频播放器和路由 +const source = audioPlayer.createBufferSource(); +const route = audioPlayer.createRoute(audioSource); + +// 配置效果链 +const stereo = audioPlayer.createStereoEffect(); +const echo = audioPlayer.createEchoEffect(); +const volume = audioPlayer.createVolumeEffect(); + +route.addEffect([stereo, echo], 0); // 插入到链首 +route.addEffect(volume); // 音量控制放到链尾 + +// 播放暂停 +await route.play(); +await route.pause(); +route.resume(); // 继续操作不是异步,不需要 await +await route.stop(); +``` + +--- + +## 处理流程示意图 + +```mermaid +sequenceDiagram + participant User + participant AudioRoute + participant Effects + + User->>AudioRoute: play() + AudioRoute->>Effects: 启动所有效果器 + Effects-->>AudioRoute: 准备完成 + AudioRoute->>Source: 开始播放 + loop 播放中 + AudioRoute->>Effects: 实时音频处理 + end + User->>AudioRoute: pause() + AudioRoute->>Effects: 启动淡出过渡 + Effects-->>AudioRoute: 过渡完成 + AudioRoute->>Source: 暂停播放 +``` diff --git a/docs/api/user-client-modules/AudioSource.md b/docs/api/user-client-modules/AudioSource.md new file mode 100644 index 0000000..e3c0e20 --- /dev/null +++ b/docs/api/user-client-modules/AudioSource.md @@ -0,0 +1,184 @@ +# AudioSource API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +--- + +## 类描述 + +音频系统的源头抽象类,定义了音频播放的核心控制接口。支持多种音频源类型,包括流媒体、HTML 音频元素和静态音频缓冲。 + +```mermaid +graph LR + AudioPlayer --> EventEmitter + + click EventEmitter "https://nodejs.org/api/events.html#class-eventemitter" +``` + +--- + +## 抽象成员说明 + +| 成员 | 类型 | 说明 | +| ------------- | ----------- | ------------------------ | +| `output` | `AudioNode` | 音频输出节点(必须实现) | +| `duration` | `number` | 音频总时长(秒) | +| `currentTime` | `number` | 当前播放时间(秒) | +| `playing` | `boolean` | 播放状态标识 | + +--- + +## 核心方法说明 + +### `abstract play` + +```typescript +function play(when?: number): void; +``` + +启动音频播放时序 + +| 参数 | 类型 | 说明 | +| ---- | -------- | ----------------------------------------------- | +| when | `number` | 预定播放时间(基于 `AudioContext.currentTime`) | + +--- + +### `abstract stop` + +```typescript +function stop(): number; +``` + +停止播放并返回停止时刻 + +--- + +### `abstract connect` + +```typescript +function connect(target: IAudioInput): void; +``` + +连接至音频处理管线 + +| 参数 | 类型 | 说明 | +| ------ | ------------- | ------------------- | +| target | `IAudioInput` | 下游处理节点/效果器 | + +--- + +### `abstract setLoop` + +```typescript +function setLoop(loop: boolean): void; +``` + +设置循环播放模式 + +--- + +## 事件系统 + +| 事件名 | 参数 | 触发时机 | +| ------ | ---- | -------------- | +| `play` | - | 开始播放时 | +| `end` | - | 自然播放结束时 | + +--- + +## 自定义音频源示例 + +### 网络实时通话源 + +```typescript +class WebRTCAudioSource extends AudioSource { + private mediaStream: MediaStreamAudioSourceNode; + output: AudioNode; + + constructor(ac: AudioContext, stream: MediaStream) { + super(ac); + this.mediaStream = ac.createMediaStreamSource(stream); + this.output = this.mediaStream; + } + + get duration() { + return Infinity; + } // 实时流无固定时长 + get currentTime() { + return this.ac.currentTime; + } + + play() { + this.mediaStream.connect(this.output); + this.playing = true; + this.emit('play'); + } + + stop() { + this.mediaStream.disconnect(); + this.playing = false; + return this.ac.currentTime; + } + + connect(target: IAudioInput) { + this.output.connect(target.input); + } + + setLoop() {} // 实时流不支持循环 +} + +// 使用示例 +navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => { + const source = new WebRTCAudioSource(audioContext, stream); + source.connect(effectsChain); + source.play(); +}); +``` + +--- + +## 内置实现说明 + +### AudioStreamSource(流媒体源) + +```mermaid +graph LR + Network[网络数据流] --> Buffer[缓冲区] + Buffer --> Decoder[音频解码器] + Decoder --> Output[实时音频节点] +``` + +- 支持渐进式加载 +- 动态缓冲管理 +- 适用于浏览器自身不支持的音频类型 + +### AudioElementSource(HTML 音频元素源) + +```mermaid +graph LR + AudioTag[audio 元素] -->|音频流| Output[媒体元素源节点] +``` + +- 基于 HTML5 Audio 元素 +- 支持跨域资源 +- 自动处理音频格式兼容 + +### AudioBufferSource(静态音频缓冲源) + +```mermaid +graph LR + File[音频文件] --> Decode[解码为 AudioBuffer] + Decode --> Output[缓冲源节点] +``` + +- 完整音频数据预加载 +- 精确播放控制 +- 支持内存音频播放 + +--- + +## 注意事项 + +1. **时间精度** + 所有时间参数均以 `AudioContext.currentTime` 为基准,精度可达 0.01 秒 diff --git a/docs/api/user-client-modules/BgmController.md b/docs/api/user-client-modules/BgmController.md new file mode 100644 index 0000000..0930c54 --- /dev/null +++ b/docs/api/user-client-modules/BgmController.md @@ -0,0 +1,158 @@ +# BgmController API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +```mermaid +graph LR + BgmController --> EventEmitter + + click EventEmitter "https://nodejs.org/api/events.html#class-eventemitter" +``` + +## 类描述 + +`BgmController` 是背景音乐系统的核心控制器,支持多 BGM 的加载、音量控制、渐变切换和播放状态管理。继承自 `EventEmitter`,提供完整的音频事件监听机制。 + +--- + +## 泛型说明 + +- `T extends string`: BGM 的唯一标识符类型(默认为项目预定义的 `BgmIds`) + +--- + +## 属性说明 + +| 属性名 | 类型 | 描述 | +| ---------------- | --------- | ----------------------------------------- | +| `prefix` | `string` | BGM 资源路径前缀(默认 `bgms.`) | +| `playingBgm` | `T` | 当前正在播放的 BGM ID | +| `enabled` | `boolean` | 是否启用音频控制(默认 true) | +| `transitionTime` | `number` | 音频切换渐变时长(单位:毫秒,默认 2000) | + +--- + +## 核心方法说明 + +### `setTransitionTime` + +```typescript +function setTransitionTime(time: number): void; +``` + +设置所有 BGM 的渐变切换时长。 + +- **参数** + - `time`: 渐变时长(毫秒) + +--- + +### `blockChange` + +```typescript +function blockChange(): void; +``` + +### `unblockChange` + +```typescript +function unblockChange(): void; +``` + +屏蔽/解除屏蔽 BGM 切换(用于特殊场景)。 + +--- + +### `setVolume` + +```typescript +function setVolume(volume: number): void; +``` + +### `getVolume` + +```typescript +function getVolume(): number; +``` + +控制全局音量(范围 0-1)。 + +--- + +### `setEnabled` + +```typescript +function setEnabled(enabled: boolean): void; +``` + +启用/禁用整个 BGM 系统(禁用时停止所有播放)。 + +--- + +### `addBgm` + +```typescript +function addBgm(id: T, url?: string): void; +``` + +加载并注册 BGM 资源。 + +- **参数** + - `id`: BGM 唯一标识 + - `url`: 自定义资源路径(默认 `project/bgms/${id}`) + +--- + +### `removeBgm` + +```typescript +function removeBgm(id: T): void; +``` + +移除已注册的 BGM 资源。 + +--- + +### 播放控制方法 + +```typescript +function play(id: T, when?: number): void; // 播放指定 BGM(带渐变) +function pause(): void; // 暂停当前 BGM(保留进度) +function resume(): void; // 继续播放当前 BGM +function stop(): void; // 停止当前 BGM(重置进度) +``` + +--- + +## 事件说明 + +| 事件名 | 参数 | 触发时机 | +| -------- | ---- | ----------------- | +| `play` | `[]` | 开始播放新 BGM 时 | +| `pause` | `[]` | 暂停播放时 | +| `resume` | `[]` | 继续播放时 | +| `stop` | `[]` | 完全停止播放时 | + +--- + +## 总使用示例 + +```typescript +import { bgmController } from '@user/client-modules'; + +// 设置全局参数 +bgmCtrl.setTransitionTime(1500); +bgmCtrl.setVolume(0.8); + +// 播放控制 +bgmCtrl.play('battle.mp3'); // 播放战斗BGM +bgmCtrl.pause(); // 暂停(如打开菜单) +bgmCtrl.resume(); // 继续播放 +bgmCtrl.play('boss_battle.mp3'); // 切换至BOSS战BGM +bgmCtrl.stop(); // 完全停止(如战斗结束) + +// 事件监听 +bgmCtrl.on('play', () => { + console.log('BGM 开始播放:', bgmCtrl.playingBgm); +}); +``` diff --git a/docs/api/user-client-modules/HeroKeyMover.md b/docs/api/user-client-modules/HeroKeyMover.md new file mode 100644 index 0000000..a5e5350 --- /dev/null +++ b/docs/api/user-client-modules/HeroKeyMover.md @@ -0,0 +1,147 @@ +# HeroKeyMover API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +## 类描述 + +`HeroKeyMover` 是勇士按键移动的核心控制器,负责将热键系统与勇士移动逻辑结合,实现基于键盘输入的连续移动控制。支持多方向优先级处理和移动中断机制。 + +--- + +## 属性说明 + +| 属性名 | 类型 | 描述 | +| ------------ | ----------- | ---------------------------------------- | +| `hotkey` | `Hotkey` | 关联的热键控制器实例 | +| `mover` | `HeroMover` | 勇士移动逻辑执行器 | +| `scope` | `symbol` | 当前移动触发的作用域(默认使用主作用域) | +| `hotkeyData` | `MoveKey` | 移动方向与热键的映射配置 | + +--- + +## 构造方法 + +```typescript +function constructor( + hotkey: Hotkey, + mover: HeroMover, + config?: MoveKeyConfig +): HeroKeyMover; +``` + +- **参数** + - `hotkey`: 已配置的热键控制器实例 + - `mover`: 勇士移动逻辑实例 + - `config`: 自定义方向键映射配置(可选) + +**默认按键映射**: + +```typescript +const map = { + left: 'moveLeft', + right: 'moveRight', + up: 'moveUp', + down: 'moveDown' +}; +``` + +--- + +## 方法说明 + +### `setScope` + +```typescript +function setScope(scope: symbol): void; +``` + +设置当前移动控制的作用域(用于多场景隔离)。 + +- **参数** + - `scope`: 唯一作用域标识符 + +--- + +### `press` + +```typescript +function press(dir: Dir): void; +``` + +触发指定方向的移动按键按下状态。 + +- **参数** + - `dir`: 移动方向(`'left' | 'right' | 'up' | 'down'`) + +--- + +### `release` + +```typescript +function release(dir: Dir): void; +``` + +解除指定方向的移动按键按下状态。 + +- **参数** + - `dir`: 要释放的移动方向 + +--- + +### `tryStartMove` + +```typescript +function tryStartMove(): boolean; +``` + +尝试启动移动逻辑(自动根据当前方向键状态判断)。 + +- **返回值** + `true` 表示移动成功启动,`false` 表示条件不满足 + +--- + +### `endMove` + +```typescript +function endMove(): void; +``` + +立即终止当前移动过程。 + +--- + +### `destroy` + +```typescript +function destroy(): void; +``` + +销毁控制器实例(自动解除所有事件监听)。 + +--- + +## 总使用示例 + +```typescript +import { gameKey, mainScope } from '@motajs/system-action'; + +// 初始化移动控制器 +const keyMover = new HeroKeyMover( + gameKey, + heroMover, + { left: 'moveLeft', right: 'moveRight' } // 自定义部分按键映射 +); + +// 设置允许触发的作用域 +keyMover.setScope(mainScope); + +// 销毁控制器 +keyMover.destroy(); +``` + +## 移动优先级机制 + +1. **最后按下优先**:当同时按下多个方向键时,以后按下的方向为准 +2. **队列延续**:在移动过程中持续检测按键状态,自动延续移动队列 +3. **作用域隔离**:只有当前作用域匹配时才会响应按键事件 diff --git a/docs/api/user-client-modules/SoundPlayer.md b/docs/api/user-client-modules/SoundPlayer.md new file mode 100644 index 0000000..28f36da --- /dev/null +++ b/docs/api/user-client-modules/SoundPlayer.md @@ -0,0 +1,197 @@ +# SoundPlayer API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +--- + +## 类描述 + +音效管理核心类,提供短音频的加载、播放和空间化控制功能。推荐通过全局单例 `soundPlayer` 使用。 + +```mermaid +graph LR + AudioPlayer --> EventEmitter + + click EventEmitter "https://nodejs.org/api/events.html#class-eventemitter" +``` + +--- + +## 属性说明 + +| 属性名 | 类型 | 说明 | +| --------- | --------------------- | ---------------------- | +| `enabled` | `boolean` | 总开关状态(默认启用) | +| `buffer` | `Map` | 已加载音效缓冲存储池 | +| `playing` | `Set` | 当前活跃音效 ID 集合 | +| `gain` | `VolumeEffect` | 全局音量控制器 | + +--- + +## 方法说明 + +### 基础控制 + +#### setEnabled + +```typescript +function setEnabled(enabled: boolean): void; +``` + +启用/禁用音效系统(禁用时立即停止所有音效) + +| 参数 | 类型 | 说明 | +| ------- | --------- | ------------ | +| enabled | `boolean` | 是否启用音效 | + +--- + +#### setVolume / getVolume + +```typescript +function setVolume(volume: number): void; +function getVolume(): number; +``` + +全局音量控制(范围 0.0~1.0) + +--- + +### 资源管理 + +#### add + +```typescript +async function add(id: T, data: Uint8Array): Promise; +``` + +加载并缓存音效资源 + +| 参数 | 类型 | 说明 | +| ---- | ------------ | ---------------- | +| id | `T` | 音效唯一标识符 | +| data | `Uint8Array` | 原始音频字节数据 | + +--- + +### 播放控制 + +#### play + +```typescript +function play( + id: T, + position?: [number, number, number], + orientation?: [number, number, number] +): number; +``` + +播放指定音效(返回音效实例 ID) + +| 参数 | 类型 | 默认值 | 说明 | +| ----------- | ----------- | --------- | ------------------------- | +| id | `T` | - | 音效标识符 | +| position | `[x, y, z]` | `[0,0,0]` | 3D 空间坐标(右手坐标系) | +| orientation | `[x, y, z]` | `[1,0,0]` | 声音传播方向向量 | + +**坐标系说明**: + +```txt +(0,0,0) 听者位置 +X+ → 右 +Y+ ↑ 上 +Z+ ⊙ 朝向听者正前方 +``` + +--- + +#### stop + +```typescript +function stop(num: number): void; +``` + +停止指定音效实例 + +| 参数 | 类型 | 说明 | +| ---- | -------- | -------------------- | +| num | `number` | play() 返回的实例 ID | + +--- + +#### stopAllSounds + +```typescript +function stopAllSounds(): void; +``` + +立即停止所有正在播放的音效 + +--- + +## 使用示例 + +### 基础音效系统 + +```typescript +import { soundPlayer } from '@user/client-modules'; + +// 播放射击音效(右侧声场) +const shotId = soundPlayer.play('shoot', [2, 0, 0]); + +// 播放爆炸音效(左后方) +soundPlayer.play('explosion', [-3, 0, -2], [-1, 0, -1]); + +// 停止特定音效 +soundPlayer.stop(shotId); + +// 全局音量控制 +soundPlayer.setVolume(0.7); +``` + +### 3D 环境音效 + +```typescript +// 汽车引擎循环音效 +let engineSoundId = -1; + +function startEngine() { + engineSoundId = soundPlayer.play('engine', [0, 0, -5]); +} + +function updateCarPosition(x: number, z: number) { + const route = audioPlayer.getRoute(`sounds.${engineSoundId}`); + const stereo = route?.effectRoute[0] as StereoEffect; + stereo?.setPosition(x, 0, z); +} +``` + +--- + +## 生命周期管理 + +```mermaid +sequenceDiagram + participant User + participant SoundPlayer + participant AudioPlayer + + User->>SoundPlayer: add('explosion', data) + SoundPlayer->>AudioPlayer: decodeAudioData() + AudioPlayer-->>SoundPlayer: AudioBuffer + User->>SoundPlayer: play('explosion') + SoundPlayer->>AudioPlayer: 创建路由/效果器 + AudioPlayer-->>SoundPlayer: 音效ID + loop 播放周期 + SoundPlayer->>AudioPlayer: 更新空间参数 + end + User->>SoundPlayer: stop(id) + SoundPlayer->>AudioPlayer: 释放路由资源 +``` + +--- + +## 注意事项 + +1. **实例数量限制** + 同时播放音效建议不超过 32 个,可通过优先级系统管理 diff --git a/docs/api/user-client-modules/StreamLoader.md b/docs/api/user-client-modules/StreamLoader.md new file mode 100644 index 0000000..f07ea57 --- /dev/null +++ b/docs/api/user-client-modules/StreamLoader.md @@ -0,0 +1,208 @@ +# StreamLoader API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +```mermaid +graph LR + StreamLoader --> EventEmitter + + click EventEmitter "https://nodejs.org/api/events.html#class-eventemitter" +``` + +## 类描述 + +`StreamLoader` 是流式加载大文件的核心类,支持分块读取网络资源并通过事件机制传递数据。继承自 `EventEmitter`,实现 `IStreamController` 接口,提供流传输控制能力。 + +--- + +## 属性说明 + +| 属性名 | 类型 | 描述 | +| --------- | --------- | ---------------------- | +| `url` | `string` | 只读,要加载的资源 URL | +| `loading` | `boolean` | 当前是否处于加载状态 | + +--- + +## 构造方法 + +```typescript +function constructor(url: string): StreamLoader; +``` + +- **参数** + - `url`: 要加载的资源地址 + +**示例** + +```typescript +const loader = new StreamLoader('/api/large-file'); +``` + +--- + +## 方法说明 + +### `pipe` + +```typescript +function pipe(reader: IStreamReader): this; +``` + +将流数据管道传递给读取器对象。 + +- **参数** + - `reader`: 实现 `IStreamReader` 接口的对象 + +**示例** + +```typescript +class MyReader implements IStreamReader { + async pump(data, done) { + console.log('收到数据块:', data); + } + // ... 还有一些其他需要实现的方法,参考总是用示例 +} +loader.pipe(new MyReader()); +``` + +--- + +### `start` + +```typescript +function start(): Promise; +``` + +启动流传输流程(自动处理分块读取与分发)。 + +--- + +### `cancel` + +```typescript +function cancel(reason?: string): void; +``` + +终止当前流传输。 + +- **参数** + - `reason`: 终止原因描述(可选) + +**示例** + +```typescript +// 用户取消加载 +loader.cancel('用户手动取消'); +``` + +--- + +## 事件说明 + +| 事件名 | 参数类型 | 触发时机 | +| ------ | --------------------------------- | ------------------------ | +| `data` | `data: Uint8Array, done: boolean` | 每接收到一个数据块时触发 | + +**事件监听示例** + +```typescript +loader.on('data', (data, done) => { + if (done) console.log('传输完成'); +}); +``` + +--- + +## 相关接口说明 + +### IStreamReader + +```typescript +export interface IStreamReader { + /** + * 接受字节流流传输的数据 + * @param data 传入的字节流数据,只包含本分块的内容 + * @param done 是否传输完成 + */ + pump( + data: Uint8Array | undefined, + done: boolean, + response: Response + ): Promise; + + /** + * 当前对象被传递给加载流时执行的函数 + * @param controller 传输流控制对象 + */ + piped(controller: IStreamController): void; + + /** + * 开始流传输 + * @param stream 传输流对象 + * @param controller 传输流控制对象 + */ + start( + stream: ReadableStream, + controller: IStreamController, + response: Response + ): Promise; + + /** + * 结束流传输 + * @param done 是否传输完成,如果为 false 的话,说明可能是由于出现错误导致的终止 + * @param reason 如果没有传输完成,那么表示失败的原因 + */ + end(done: boolean, reason?: string): void; +} +``` + +- `pump`: 处理每个数据块 +- `piped`: 当读取器被绑定到流时调用 +- `start`: 流传输开始时调用 +- `end`: 流传输结束时调用 + +--- + +## 总使用示例 + +```typescript +// 创建流加载器 +const loader = new StreamLoader('/api/video-stream'); + +const videoElement = document.createElement('video'); + +// 实现自定义读取器 +class VideoStreamReader implements IStreamReader { + async pump(data, done) { + if (data) videoElement.appendBuffer(data); + if (done) videoElement.play(); + } + + piped(controller) { + console.log('流传输管道连接成功'); + } + + start() { + console.log('开始流式加载'); + } + + end() { + console.log('流式加载结束'); + } +} + +const reader = new VideoStreamReader(); + +// 绑定读取器并启动 +loader.pipe(reader); +loader.start(); + +// 监听进度 +loader.on('data', (_, done) => { + if (!done) updateProgressBar(); +}); + +// 错误处理 +videoElement.onerror = () => loader.cancel('视频解码错误'); +``` diff --git a/docs/api/user-client-modules/TextContentParser.md b/docs/api/user-client-modules/TextContentParser.md new file mode 100644 index 0000000..b01cbae --- /dev/null +++ b/docs/api/user-client-modules/TextContentParser.md @@ -0,0 +1,157 @@ +# TextContentParser API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +## 类描述 + +`TextContentParser` 是文字解析核心工具,用于处理文本排版、转义字符解析及动态样式管理。支持自动分词换行、图标嵌入和样式栈控制。 + +--- + +## 方法说明 + +### `parse` + +```typescript +function parse(text: string, width: number): ITextContentRenderObject; +``` + +解析文本并生成渲染数据对象: + +```typescript +interface ITextContentRenderObject { + lineHeights: number[]; // 每行高度 + lineWidths: number[]; // 每行宽度 + data: ITextContentRenderable[]; // 渲染元素集合 +} +``` + +--- + +## 转义字符语法说明 + +### 1. 颜色控制 `\r[color]` + +- **语法**:`\r[颜色值]` +- **栈模式**:支持嵌套,用`\r`恢复上一级颜色 +- **颜色格式**:支持 CSS 颜色字符串 + +```typescript +// 示例:红→黄→红→默认 +'\\r[red]危险!\\r[yellow]警告\\r恢复红色\\r默认'; +``` + +### 2. 字号控制 `\c[size]` + +- **语法**:`\c[字号(px)]` +- **栈模式**:用`\c`恢复上一级字号 + +```typescript +// 示例:24px→32px→24px +'普通\\c[24]标题\\c[32]超大标题\\c恢复'; +``` + +### 3. 字体家族 `\g[family]` + +- **语法**:`\g[字体名称]` +- **栈模式**:用`\g`恢复上一级字体 + +```typescript +'默认\\g[黑体]中文黑体\\g恢复默认'; +``` + +### 4. 粗体切换 `\d` + +- **语法**:`\d`(开关模式) + +```typescript +'正常\\d粗体\\d正常'; +``` + +### 5. 斜体切换 `\e` + +- **语法**:`\e`(开关模式) + +```typescript +'正常\\e斜体\\e正常'; +``` + +### 6. 等待间隔 `\z[wait]` + +- **语法**:`\z[等待字符数]` +- **计算规则**:`间隔时间 = 字符数 * 当前interval配置` + +```typescript +'开始对话\\z[10](暂停500ms)继续'; +``` + +### 7. 图标嵌入 `\i[icon]` + +- **语法**:`\i[图标ID]` +- **图标规范**:需预加载到资源管理器 + +```typescript +'攻击\\i[sword]造成伤害'; +``` + +### 8. 表达式 `${}` + +- **语法**:与模板字符串语法一致,不过是在渲染的时候实时计算,而非字符串声明时计算 + +```typescript +'${core.status.hero.atk * 10}'; // 显示勇士攻击乘 10 +'${core.status.hero.atk > 100 ? "高攻击" : "低攻击"}'; // 条件表达式 +'${(() => { if (a > 10) return 100; else return 10; })()}'; // 嵌入函数 +``` + +--- + +## 综合使用示例 + +### 战斗伤害提示 + +```typescript +const text = + '\\r[#ff0000]\\c[24]\\d敌人\\i[monster]对你造成\\c[32]\\r[yellow]500\\c\\r伤害!\\z[5]\\d\\e(按空格跳过)'; + +const result = parser.parse(text, 400); + +/* 解析结果: +[ + { type: 'text', color: '#ff0000', size:24, bold:true, text:'敌人' }, + { type: 'icon', id:'monster' }, + { type: 'text', color:'#ff0000', size:24, text:'对你造成' }, + { type: 'text', color:'yellow', size:32, text:'500' }, + { type: 'text', color:'#ff0000', size:24, text:'伤害!' }, + { type: 'wait', duration:250 }, // 假设 interval=50 + { type: 'text', bold:false, italic:true, text:'(按空格跳过)' } +] +*/ +``` + +### 多语言混合排版 + +```typescript +const multiLangText = + '\\g[Times New Roman]Hello\\g[宋体]你好\\i[globe]\\z[3]\\g切换为\\r[blue]Français'; + +// 效果:英文→中文+地球图标→等待→蓝色法文 +``` + +--- + +## 注意事项 + +1. **转义字符格式** + + - 必须使用 **双反斜杠**(`\\`)表示转义 + - 错误示例:`\r[red]`(单反斜杠 `\r` 可能会被识别为换行) + - 正确示例:`\\r[red]` + +2. **栈操作规则** + + ```typescript + // 颜色栈示例 + '默认\\r[red]红\\r[blue]蓝\\r恢复红\\r恢复默认'; + // 等效于:push(默认)→push(红)→push(蓝)→pop→pop + ``` diff --git a/docs/api/user-client-modules/TextContentTyper.md b/docs/api/user-client-modules/TextContentTyper.md new file mode 100644 index 0000000..9f76150 --- /dev/null +++ b/docs/api/user-client-modules/TextContentTyper.md @@ -0,0 +1,245 @@ +# TextContentTyper API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +```mermaid +graph LR + TextContentTyper --> EventEmitter + + click EventEmitter "https://nodejs.org/api/events.html#class-eventemitter" +``` + +## 类描述 + +`TextContentTyper` 是文字逐字输出(打字机效果)的核心控制器,继承自 `EventEmitter`。用于管理文字排版、渲染时序及样式配置,支持动态修改文本内容和样式。 + +--- + +## 核心属性说明 + +| 属性名 | 类型 | 描述 | +| -------- | ----------------------- | ---------------------------------------------- | +| `config` | `Required` | 当前文字渲染配置(包含字体、行高、对齐等参数) | +| `parser` | `TextContentParser` | 文字解析器实例(负责分词、分行等底层处理) | + +--- + +## 核心方法说明 + +### `constructor` + +```typescript +function constructor(config: Partial): TextContentTyper; +``` + +初始化打字机实例,接受文字配置参数: + +```typescript +interface ITextContentConfig { + /** 字体 */ + font: Font; + /** 是否持续上一次的文本,开启后,如果修改后的文本以修改前的文本为开头,那么会继续播放而不会从头播放(暂未实现,后续更新) */ + keepLast: boolean; + /** 打字机时间间隔,即两个字出现之间相隔多长时间 */ + interval: number; + /** 行高 */ + lineHeight: number; + /** 分词规则 */ + wordBreak: WordBreak; + /** 文字对齐方式 */ + textAlign: TextAlign; + /** 行首忽略字符,即不会出现在行首的字符 */ + ignoreLineStart: Iterable; + /** 行尾忽略字符,即不会出现在行尾的字符 */ + ignoreLineEnd: Iterable; + /** 会被分词规则识别的分词字符 */ + breakChars: Iterable; + /** 填充样式 */ + fillStyle: CanvasStyle; + /** 描边样式 */ + strokeStyle: CanvasStyle; + /** 线宽 */ + strokeWidth: number; + /** 文字宽度,到达这么宽之后换行 */ + width: number; +} +``` + +--- + +### `setConfig` + +```typescript +function setConfig(config: Partial): void; +``` + +动态更新配置参数(支持部分更新) + +--- + +### `setText` + +```typescript +function setText(text: string): void; +``` + +设置要显示的文本内容(自动重置播放进度) + +--- + +### `type` + +```typescript +function type(): void; +``` + +启动逐字显示效果 + +--- + +### `typeAll` + +```typescript +function typeAll(): void; +``` + +立即完整显示所有文字 + +--- + +### `setRender` + +```typescript +function setRender(render: TyperFunction): void; +``` + +设置自定义渲染逻辑: + +```typescript +type TyperFunction = ( + data: TyperRenderable[], // 待渲染元素 + typing: boolean // 是否正在播放中 +) => void; +``` + +--- + +## 事件说明 + +| 事件名 | 参数 | 触发时机 | +| ----------- | ---- | ------------------ | +| `typeStart` | `[]` | 开始逐字显示时 | +| `typeEnd` | `[]` | 全部文字显示完成时 | + +--- + +## 使用示例 + +### 基础用法 - 对话框文字 + +```typescript +// 初始化配置 +const typer = new TextContentTyper({ + font: new Font('黑体', 18), + interval: 50, + lineHeight: 1.2, + width: 400 +}); + +// 设置文本内容 +typer.setText(`「这是逐字显示的文字效果... + 第二行会自动换行」`); + +// 注册渲染逻辑 +typer.setRender((elements, isTyping) => { + elements.forEach(element => { + if (element.type === TextContentType.Text) { + drawText(element.x, element.y, element.text); + } + }); +}); + +// 开始播放 +typer.type(); +``` + +### 动态样式修改 + +```typescript +// 修改为红色斜体 +typer.setConfig({ + font: new Font('楷体', 20), + fillStyle: '#ff0000' +}); + +// 修改播放速度 +typer.setConfig({ interval: 30 }); +``` + +--- + +## 底层数据结构 + +### 渲染元素类型 + +```typescript +type TyperRenderable = + | TyperTextRenderable // 文本元素 + | TyperIconRenderable // 图标元素 + | TyperWaitRenderable; // 等待间隔 +``` + +::: code-group + +```ts [TyperTextRenderable] +interface TyperTextRenderable { + type: TextContentType.Text; + x: number; + y: number; + text: string; + font: string; + fillStyle: CanvasStyle; + strokeStyle: CanvasStyle; + /** 文字画到哪个索引 */ + pointer: number; + /** 这段文字的总高度 */ + height: number; +} +``` + +```ts [TyperIconRenderable] +interface TyperIconRenderable { + type: TextContentType.Icon; + x: number; + y: number; + width: number; + height: number; + renderable: RenderableData | AutotileRenderable; +} +``` + +```ts [TyperWaitRenderable] +interface TyperWaitRenderable { + type: TextContentType.Wait; + wait: number; + waited: number; +} +``` + +::: + +--- + +## 注意事项 + +1. **性能优化** + 当处理长文本(>1000 字)时,建议预调用 `parser.parse()` 进行分页 + +2. **坐标系统** + 所有坐标基于初始化时设置的 `width` 参数进行相对计算 + +3. **动态修改限制** + 在播放过程中修改配置可能导致渲染异常,建议在 `typeEnd` 事件后操作 + +4. **使用场景** + 本接口的使用场景并不多,建议使用 `TextContent` 组件。如果必须使用的话,可以直接阅读源码来看一些实现细节。 diff --git a/docs/api/user-client-modules/TextboxStore.md b/docs/api/user-client-modules/TextboxStore.md new file mode 100644 index 0000000..c6a8451 --- /dev/null +++ b/docs/api/user-client-modules/TextboxStore.md @@ -0,0 +1,166 @@ +# TextboxStore API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +--- + +```mermaid +graph LR + TextboxStore --> EventEmitter + + click EventEmitter "https://nodejs.org/api/events.html#class-eventemitter" +``` + +--- + +## 类描述 + +`TextboxStore` 是文本框的集中管理器,继承自 `EventEmitter`。所有 `Textbox` 组件实例化时会自动注册到该类的静态 `list` 中,支持通过 ID 精准控制特定文本框。 + +--- + +## 核心方法说明 + +### `TextboxStore.get` + +```typescript +function get(id: string): TextboxStore | undefined; +``` + +**静态方法**:通过 ID 获取已注册的文本框控制器 + +- **参数** + `id`: 文本框的唯一标识符 +- **返回值** + 找到返回实例,否则返回 `undefined` + +--- + +### `setText` + +```typescript +function setText(text: string): void; +``` + +**动态更新文本内容** + +- **特性** + - 自动重置打字机进度 + - 触发重新排版计算 + +--- + +### `modify` + +```typescript +function modify(data: Partial): void; +``` + +**动态修改文本框配置** + +- **参数** + `data`: 需更新的属性(支持所有 `TextboxProps` 属性) + +--- + +### `endType` + +```typescript +function endType(): void; +``` + +**立即结束打字机动画** + +- **特性** + - 强制显示全部文本 + - 触发 `typeEnd` 事件 + +--- + +### `show` + +```ts +function show(): void; +``` + +### `hide` + +```ts +function hide(): void; +``` + +控制文本框的显示和隐藏。 + +--- + +## 使用示例 + +### 跨场景更新对话内容 + +```typescript +// 在剧情管理器中的调用 +const updateChapterDialog = (chapterId: string) => { + const store = TextboxStore.get(`chapter_${chapterId}`); + store?.setText(getChapterText(chapterId)); + store?.modify({ title: `第 ${chapterId} 章` }); +}; +``` + +### 紧急提示打断当前动画 + +```typescript +// 强制显示关键信息 +const showEmergencyAlert = () => { + const alertBox = TextboxStore.get('system_alert'); + alertBox?.setText('警告!基地即将爆炸!'); + alertBox?.endType(); // 跳过打字动画 + alertBox?.show(); +}; +``` + +### 动态样式调整 + +```typescript +// 根据昼夜切换对话框样式 +const updateDialogStyle = (isNight: boolean) => { + TextboxStore.list.forEach(store => { + store.modify({ + backColor: isNight ? '#1A1A32' : '#F0F0FF', + titleFill: isNight ? '#E6E6FA' : '#2F4F4F' + }); + }); +}; +``` + +--- + +## 注意事项 + +1. **ID 管理规范** + 建议显式指定可预测的 ID 格式: + + ```tsx + // 创建时指定可追踪 ID + + ``` + +2. **未找到实例处理** + 调用前需做空值检测: + + ```typescript + const store = TextboxStore.get('custom_id'); + if (!store) { + console.warn('文本框未注册: custom_id'); + return; + } + ``` + +3. **生命周期匹配** + 在组件卸载时自动注销实例,请勿持有长期引用 + +4. **批量操作优化** + 同时操作多个实例时建议使用迭代器: + ```typescript + // 隐藏所有对话框 + TextboxStore.list.forEach(store => store.hide()); + ``` diff --git a/docs/api/user-client-modules/TipStore.md b/docs/api/user-client-modules/TipStore.md new file mode 100644 index 0000000..d8b7882 --- /dev/null +++ b/docs/api/user-client-modules/TipStore.md @@ -0,0 +1,147 @@ +# TipStore API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +--- + +## 类描述 + +`TipStore` 是提示框的集中管理器,提供全局访问和控制提示组件的能力。所有通过 `` 组件注册的实例会自动加入静态 `list` 容器,支持通过 ID 精准控制特定提示框。 + +--- + +## 核心方法说明 + +### `TipStore.get` + +```typescript +function get(id: string): TipStore | undefined; +``` + +**静态方法**:通过 ID 获取已注册的提示框控制器 + +| 参数 | 类型 | 必填 | 说明 | +| ---- | -------- | ---- | ---------------- | +| `id` | `string` | 是 | 提示框的唯一标识 | + +**返回值**:找到返回实例,否则返回 `undefined` + +--- + +### `TipStore.use` + +```typescript +function use(id: string, data: TipExpose): TipStore; +``` + +**静态方法**:注册提示框实例到全局管理器(通常在组件内部使用) + +| 参数 | 类型 | 必填 | 说明 | +| ------ | ----------- | ---- | ----------------------- | +| `id` | `string` | 是 | 提示框的唯一标识 | +| `data` | `TipExpose` | 是 | 来自 Tip 组件的暴露接口 | + +--- + +### `drawTip` + +```typescript +function drawTip(text: string, icon?: AllIds | AllNumbers): void; +``` + +**显示提示内容**(支持带图标的提示) + +| 参数 | 类型 | 必填 | 说明 | +| ------ | ---------------------- | ---- | ------------------------------- | +| `text` | `string` | 是 | 提示文字内容 | +| `icon` | `AllIds \| AllNumbers` | 否 | 图标资源 ID(字符串或数字形式) | + +**特性**: + +- 自动触发淡入动画 +- 3 秒无操作后自动淡出 +- 重复调用会重置计时器 + +--- + +## 使用示例 + +### 基础提示 + +```typescript +// 获取预先注册的提示框 +const tip = TipStore.get('item-get-tip'); + +// 显示纯文本提示 +tip?.drawTip('获得金币 x100'); + +// 显示带图标的提示 +tip?.drawTip('获得 传说之剑', 'legend_sword'); +``` + +### 全局广播提示 + +```typescript +// 向所有提示框发送通知 +TipStore.list.forEach(store => { + store.drawTip('系统将在5分钟后维护', 'warning'); +}); +``` + +### 动态内容提示 + +```typescript +// 组合动态内容 +const showDamageTip = (damage: number) => { + TipStore.get('combat-tip')?.drawTip( + `造成 ${damage} 点伤害`, + damage > 1000 ? 'critical_hit' : 'normal_hit' + ); +}; +``` + +--- + +## 生命周期管理 + +### 组件注册流程 + +```tsx +// 在组件定义时注册实例 +; + +// 在业务逻辑中调用 +const showQuestComplete = () => { + TipStore.get('quest-tip')?.drawTip('任务「勇者的试炼」完成!'); +}; +``` + +--- + +## 注意事项 + +1. **自动清理机制** + 组件卸载时自动注销实例,跨场景访问时需确保目标组件已挂载 + +2. **错误处理** + 建议封装安全访问方法: + + ```typescript + const safeDrawTip = (id: string, text: string) => { + const instance = TipStore.get(id); + if (!instance) { + console.error(`Tip ${id} not registered`); + return; + } + instance.drawTip(text); + }; + ``` + +3. **动画队列** + 连续调用时会中断当前动画,建议重要提示添加延迟: + ```typescript + tip.drawTip('第一条提示'); + setTimeout(() => { + tip.drawTip('第二条重要提示'); + }, 3200); // 等待淡出动画结束 + ``` diff --git a/docs/api/user-client-modules/WeatherController.md b/docs/api/user-client-modules/WeatherController.md new file mode 100644 index 0000000..c8addd9 --- /dev/null +++ b/docs/api/user-client-modules/WeatherController.md @@ -0,0 +1,217 @@ +# WeatherController API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +```mermaid +graph LR + WeatherController --> IWeather +``` + +_实现 `IWeather` 接口_ + +## 类描述 + +`WeatherController` 是天气系统的核心控制器,支持动态管理多种天气效果(如雨、雪、雾等),可将天气效果绑定到任意渲染元素上,实现多场景独立天气控制。 + +--- + +## 属性说明 + +| 属性名 | 类型 | 描述 | +| -------------- | -------------------------------- | ----------------------------------------------------------------- | +| `id` | `string` | 只读,控制器的唯一标识符 | +| `active` | `Set` | 当前激活的天气实例集合 | +| `list`(静态) | `Map` | 静态属性,存储所有注册的天气类型(键为天气 ID,值为天气构造函数) | +| `map`(静态) | `Map` | 静态属性,存储所有控制器实例 | + +--- + +## 构造方法 + +```typescript +function constructor(id: string): WeatherController; +``` + +- **参数** + - `id`: 控制器的标识符 + +## 方法说明 + +### `activate` + +```typescript +function activate(id: string, level?: number): IWeather | undefined; +``` + +激活指定天气。 + +- **参数** + - `id`: 已注册的天气 ID + - `level`: 天气强度等级(可选) + +--- + +### `bind` + +```typescript +function bind(item?: RenderItem): void; +``` + +绑定/解绑渲染元素。 + +- **参数** + - `item`: 要绑定的渲染元素(不传则解绑) + +--- + +### `deactivate` + +```typescript +function deactivate(weather: IWeather): void; +``` + +关闭指定天气效果。 + +--- + +### `clearWeather` + +```typescript +function clearWeather(): void; +``` + +清空所有天气效果。 + +--- + +### `getWeather` + +```typescript +function getWeather( + weather: new (level?: number) => T +): T | null; +``` + +获取指定天气实例。 + +**示例** + +```ts +import { RainWeather } from '@user/client-modules'; + +const rain = controller.getWeather(RainWeather); +``` + +--- + +### `destroy` + +```typescript +function destroy(): void; +``` + +摧毁这个天气控制器,摧毁后不可继续使用。 + +--- + +## 静态方法说明 + +### `WeatherController.register` + +```typescript +function register(id: string, weather: Weather): void; +``` + +**静态方法**:注册新的天气类型。 + +- **参数** + - `id`: 天气唯一标识(如 "rain") + - `weather`: 天气类(需实现 `IWeather` 接口) + +--- + +### `WeatherController.get` + +```typescript +function get(id: string): WeatherController | undefined; +``` + +- **参数** + - `id`: 要获得的控制器标识符 + +## 天气接口说明 + +```typescript +interface IWeather { + activate(item: RenderItem): void; // 初始化天气效果 + frame(): void; // 每帧更新逻辑 + deactivate(item: RenderItem): void; // 清除天气效果 +} +``` + +--- + +## 内置天气 + +- `rain`: 下雨天气 + +## 总使用示例 实现滤镜天气效果 + +::: code-group + +```typescript [定义天气] +// 定义灰度滤镜天气 +class GrayFilterWeather implements IWeather { + private scale: number; + private now: number = 0; + private item?: RenderItem; + + constructor(level: number = 5) { + this.scale = level / 10; + } + + activate(item: RenderItem) { + // 添加灰度滤镜 + item.filter = `grayscale(0)`; + this.item = item; + } + + frame() { + // 动态调整滤镜强度(示例:正弦波动) + if (this.item) { + const intensity = ((Math.sin(Date.now() / 1000) + 1) * scale) / 2; + this.item.filter = `grayscale(${itensity})`; + } + } + + deactivate(item: RenderItem) { + item.filter = `none`; + } +} + +// 注册天气类型 +WeatherController.register('gray-filter', GrayFilterWeather); +``` + +```tsx [使用天气] +import { defineComponent, onMounted } from 'vue'; +import { Container } from '@motajs/render'; +import { useWeather } from '@user/client-modules'; + +const MyCom = defineComponent(() => { + const [controller] = useWeather(); + + const root = ref(); + + onMounted(() => { + // 绑定天气的渲染元素 + controller.bind(root.value); + // 激活天气效果 + controller.activate('gray-filter', 5); + }); + + return () => ; +}); +``` + +::: diff --git a/docs/api/user-client-modules/functions.md b/docs/api/user-client-modules/functions.md new file mode 100644 index 0000000..b2de6dd --- /dev/null +++ b/docs/api/user-client-modules/functions.md @@ -0,0 +1,410 @@ +# 模块函数 API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +## 钩子 + +### `onOrientationChange` + +```typescript +function onOrientationChange(hook: OrientationHook): void; +``` + +监听屏幕方向变化事件。 +**参数** + +- `hook`: 方向变化回调函数 + +```typescript +type OrientationHook = ( + orientation: Orientation, // 当前方向 + width: number, // 窗口宽度 + height: number // 窗口高度 +) => void; +``` + +**示例** - 响应式布局 + +```typescript +import { onOrientationChange, Orientation } from './use'; + +onOrientationChange((orient, width) => { + if (orient === Orientation.Portrait) { + // 竖屏模式 + adjustMobileLayout(width); + } else { + // 横屏模式 + resetDesktopLayout(); + } +}); +``` + +--- + +### `onLoaded` + +```typescript +function onLoaded(hook: () => void): void; +``` + +在游戏核心资源加载完成后执行回调(若已加载则立即执行)。 + +--- + +## 过渡动画控制 + +### 通用接口 + +```typescript +interface ITransitionedController { + readonly ref: Ref; // 响应式引用 + readonly value: T; // 当前值 + set(value: T, time?: number): void; // 设置目标值 + mode(timing: TimingFn): void; // 设置缓动曲线 + setTime(time: number): void; // 设置默认时长 +} +``` + +### `transitioned` + +```typescript +function transitioned( + value: number, // 初始值 + time: number, // 默认过渡时长(ms) + curve: TimingFn // 缓动函数(如 linear()) +): ITransitionedController | null; +``` + +创建数值渐变控制器(仅限组件内使用)。 + +**示例** - 旋转动画 + +```tsx +// Vue 组件内 +const rotate = transitioned(0, 500, hyper('sin', 'out')); + +// 触发动画 +rotate.set(Math.PI, 800); // 800ms 内旋转到 180 度 + +// 模板中使用 +; +``` + +### `transitionedColor` + +```typescript +function transitionedColor( + color: string, // 初始颜色(目前支持 #RGB/#RGBA/rgb()/rgba()) + time: number, // 默认过渡时长(ms) + curve: TimingFn // 缓动函数 +): ITransitionedController | null; +``` + +创建颜色渐变控制器(仅限组件内使用)。 + +**示例** - 背景色过渡 + +```tsx +// Vue 组件内 +const bgColor = transitionedColor('#fff', 300, linear()); + +// 触发颜色变化 +bgColor.set('rgba(255, 0, 0, 0.5)'); // 渐变为半透明红色 + +// 模板中使用 +; +``` + +--- + +### 注意事项 + +1. **组件生命周期**:过渡控制器必须在 Vue 组件内部创建,卸载时自动销毁 +2. **性能优化**:避免在频繁触发的回调(如每帧渲染)中创建新控制器 +3. **颜色格式**:`transitionedColor` 支持 HEX/RGB/RGBA,但不支持 HSL +4. **默认时长**:调用 `set()` 时不传时间参数则使用初始化时设置的时间 + +### 高级用法示例 + +#### 组合动画 + +```typescript +// 同时控制位置和透明度 +const posX = transitioned(0, 500, linear()); +const alpha = transitioned(1, 300, linear()); + +const moveAndFade = () => { + posX.set(200); + alpha.set(0); +}; + +// 组件卸载时自动清理动画资源 +``` + +## 组件控制 + +### `getConfirm` + +```typescript +function getConfirm( + controller: IUIMountable, // UI 控制器 + text: string, // 确认内容 + loc: ElementLocator, // 定位配置 + width: number, // 对话框宽度(像素) + props?: Partial // 扩展配置 +): Promise; +``` + +--- + +#### 参数说明 + +| 参数名 | 类型 | 必填 | 描述 | +| ------------ | -------------------------- | ---- | ---------------------------------------------- | +| `controller` | `IUIMountable` | 是 | UI 控制器实例(通常从组件 props 获取) | +| `text` | `string` | 是 | 需要用户确认的文本内容 | +| `loc` | `ElementLocator` | 是 | 对话框位置配置(需包含 x,y 坐标及锚点) | +| `width` | `number` | 是 | 对话框宽度(像素),高度自动计算 | +| `props` | `Partial` | 否 | 扩展配置项(支持所有 ConfirmBox 组件的 props) | + +--- + +#### 返回值 + +返回 `Promise`: + +- `true` 表示用户点击确认 +- `false` 表示用户取消或关闭 + +--- + +#### 使用示例 + +##### 基础用法 - 删除确认 + +```tsx +import { defineComponent } from 'vue'; +import { DefaultProps } from '@motajs/render'; +import { GameUI } from '@motajs/system-ui'; + +// 在业务逻辑中调用,注意,组件需要使用 UI 控制器打开,它会自动传递 controller 参数 +const MyCom = defineComponent(props => { + const handleDeleteItem = async (itemId: string) => { + const confirmed = await getConfirm( + props.controller, // 从组件 props 获取控制器 + `确认删除 ID 为 ${itemId} 的项目吗?`, + [208, 208, void 0, void 0, 0.5, 0.5], // 居中显示 + 208 + ); + + if (confirmed) { + api.deleteItem(itemId); + } + }; + + return () => ( + + {/* 假设有一个按钮在点击后触发上面的删除函数 */} + handleDeleteItem(item.id)} /> + + ); +}); + +export const MyUI = new GameUI('my-ui', MyCom); +``` + +##### 自定义按钮文本 + +```typescript +import { mainUIController } from '@user/client-modules'; +// 注意,如果在 client-modules/render/ui 下编写代码,应该引入: +import { mainUIController } from './controller.ts'; + +// 修改确认/取消按钮文案 +const result = await getConfirm( + // 传入主 UI 控制器也可以 + mainUIController, + '切换场景将丢失未保存进度', + [208, 208, void 0, void 0, 0.5, 0.5], + 320, + { + yesText: '继续切换', + noText: '留在当前', + selFill: '#e74c3c', + border: '#c0392b' + } +); +``` + +--- + +### `getChoice` + +```typescript +function getChoice( + controller: IUIMountable, // UI 控制器 + choices: ChoiceItem[], // 选项数组 + loc: ElementLocator, // 定位配置 + width: number, // 对话框宽度(像素) + props?: Partial // 扩展配置 +): Promise; +``` + +#### 参数说明 + +| 参数名 | 类型 | 必填 | 描述 | +| ------------ | ----------------------- | ---- | ------------------------------------------- | +| `controller` | `IUIMountable` | 是 | UI 控制器实例(通常从组件 props 获取) | +| `choices` | `ChoiceItem[]` | 是 | 选项数组,格式为 `[key, text]` 的元组 | +| `loc` | `ElementLocator` | 是 | 对话框位置配置(需包含 x,y 坐标及锚点) | +| `width` | `number` | 是 | 对话框宽度(像素),高度自动计算 | +| `props` | `Partial` | 否 | 扩展配置项(支持所有 Choices 组件的 props) | + +#### 返回值 + +返回 `Promise`: + +- 解析为选中项的 `key` 值 + +#### 使用示例 + +##### 基础用法 - 难度选择 + +```typescript +import { getChoice, mainUIController } from '@user/client-modules'; + +// 写到异步函数里面 +const selectedDifficulty = await getChoice( + mainUIController, + [ + ['easy', '新手模式'], + ['normal', '普通模式'], + ['hard', '困难模式'] + ], + [208, 208, void 0, void 0, 0.5, 0.5], // 居中显示 + 208, + { + title: '选择难度', + titleFont: new Font('黑体', 24) + } +); + +// 判断选择的内容 +if (selectedDifficulty === 'hard') { + applyHardcoreRules(); +} +``` + +##### 分页支持 - 角色选择 + +```typescript +import { getChoice, mainUIController } from '@user/client-modules'; + +// 生成 200 个角色选项 +const characterOptions = Array.from( + { length: 200 }, + (_, i) => [i, `角色 #${i + 1}`] as ChoiceItem +); + +const chosenId = await getChoice( + mainUIController, + characterOptions, + [208, 208, void 0, void 0, 0.5, 0.5], + 208, + { + maxHeight: 400, // 超过 400px 自动分页 + winskin: 'winskin.png', + interval: 12 + } +); +``` + +##### 动态样式配置 + +```typescript +import { getChoice, mainUIController } from '@user/client-modules'; + +// 自定义主题风格 +const choiceResult = await getChoice( + mainUIController, + [ + ['light', '浅色主题'], + ['dark', '深色主题'], + ['oled', 'OLED 深黑'] + ], + [208, 208, void 0, void 0, 0.5, 0.5], + 300, + { + color: 'rgba(30,30,30,0.9)', + border: '#4CAF50', + selFill: '#81C784', + titleFill: '#FFF59D' + } +); +``` + +### `waitbox` + +```typescript +function waitbox( + controller: IUIMountable, + loc: ElementLocator, + width: number, + promise: Promise, + props?: Partial> +): Promise; +``` + +#### 参数说明 + +| 参数名 | 类型 | 必填 | 默认值 | 描述 | +| ------------ | -------------------------- | ---- | ------ | ---------------------------------------------------- | +| `controller` | `IUIMountable` | 是 | - | UI 挂载控制器(通常传递父组件的 `props.controller`) | +| `loc` | `ElementLocator` | 是 | - | 定位参数 | +| `width` | `number` | 是 | - | 内容区域宽度(像素) | +| `promise` | `Promise` | 是 | - | 要监视的异步操作 | +| `props` | `Partial>` | 否 | `{}` | 扩展配置项(继承 `Background` + `TextContent` 属性) | + +--- + +#### 返回值 + +| 类型 | 说明 | +| ------------ | ----------------------------------------------------------------------------------- | +| `Promise` | 与传入 `Promise` 联动的代理 `Promise`,在以下情况会 `reject`:原始 `Promise` 被拒绝 | + +--- + +#### 使用示例 + +##### 等待网络请求 + +```typescript +// 获取用户数据 +const userData = await waitbox( + props.controller, + [400, 300, void 0, void 0, 0.5, 0.5], // 居中定位 + 300, + fetch('/api/user'), + { + text: '加载用户信息...', + winskin: 'ui/loading_panel' + } +); +``` + +### 注意事项 + +1. **控制器有效性** + 必须确保传入的 `controller` 已正确挂载且未销毁 + +2. **异步特性** + 需使用 `await` 或 `.then()` 处理返回的 Promise + +3. **定位系统** + Y 轴坐标基于 Canvas 坐标系(向下为正方向) + +4. **额外参考** + - [组件 ConfirmBox](./组件%20ConfirmBox.md) + - [组件 Choices](./组件%20Choices.md) + - [组件 Waitbox](./组件%20Waitbox.md) diff --git a/docs/api/user-client-modules/index.md b/docs/api/user-client-modules/index.md new file mode 100644 index 0000000..ba85d58 --- /dev/null +++ b/docs/api/user-client-modules/index.md @@ -0,0 +1,3 @@ +# @user/client-modules + +目录: diff --git a/docs/api/user-client-modules/图标组件.md b/docs/api/user-client-modules/图标组件.md new file mode 100644 index 0000000..6da0135 --- /dev/null +++ b/docs/api/user-client-modules/图标组件.md @@ -0,0 +1,37 @@ +# 图标组件 API + +## Props 属性说明 + +| 属性名 | 类型 | 必填 | 描述 | +| ------ | ---------------- | ---- | ---------- | +| `loc` | `ElementLocator` | 是 | 图标定位符 | + +图标比例固定,会自动根据传入的长宽缩放。 + +## 图标列表 + +- `RollbackIcon`: 回退图标 +- `RetweenIcon`: 回收图标 +- `ViewMapIcon`: 浏览地图图标 +- `DanmakuIcon`: 弹幕图标 +- `ReplayIcon`: 回放图标 +- `numpadIcon`: 数字键盘图标 +- `PlayIcon`: 开始播放图标 +- `PauseIcon`: 暂停播放图标 +- `DoubleArrow`: 双箭头图标(向右) +- `StepForward`: 单步向前图标 + +## 使用示例 + +```tsx +import { defineComponent } from 'vue'; +import { RollbackIcon } from '@user/client-modules'; + +export const MyCom = defineComponent(() => { + return () => ( + + + + ); +}); +``` diff --git a/docs/api/user-client-modules/组件 Arrow.md b/docs/api/user-client-modules/组件 Arrow.md new file mode 100644 index 0000000..fca36f8 --- /dev/null +++ b/docs/api/user-client-modules/组件 Arrow.md @@ -0,0 +1,50 @@ +# Arrow 组件 API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +--- + +## 核心特性 + +- **两点连线**:通过坐标点绘制任意方向箭头 +- **头部定制**:可调节箭头尖端大小 +- **样式控制**:支持颜色自定义 + +--- + +## Props 属性说明 + +| 属性名 | 类型 | 默认值 | 描述 | +| ------- | ---------------------------------- | ------ | ----------------------------------------- | +| `arrow` | `[number, number, number, number]` | 必填 | 箭头坐标 [起点 x, 起点 y, 终点 x, 终点 y] | +| `head` | `number` | `8` | 箭头头部尺寸(像素) | +| `color` | `CanvasStyle` | `#fff` | 箭头颜色 | + +--- + +## 使用示例 + +### 基础箭头 + +```tsx +// 从 (50, 100) 到 (200, 300) 的基础箭头 + +``` + +### 定制样式 + +```tsx +// 红色粗箭头(头部尺寸12px) + +``` + +### 虚线箭头 + +```tsx +// 带虚线效果的箭头 + +``` diff --git a/docs/api/user-client-modules/组件 Background.md b/docs/api/user-client-modules/组件 Background.md new file mode 100644 index 0000000..37a6985 --- /dev/null +++ b/docs/api/user-client-modules/组件 Background.md @@ -0,0 +1,78 @@ +# Background 背景组件 API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +--- + +## 核心特性 + +- **双样式模式**:支持图片皮肤或纯色填充 +- **精准定位**:像素级坐标控制 +- **静态呈现**:无内置动画的稳定背景层 + +--- + +## Props 属性说明 + +| 属性名 | 类型 | 默认值 | 描述 | +| --------- | ---------------- | -------- | ----------------------------- | +| `loc` | `ElementLocator` | **必填** | 背景定位 | +| `winskin` | `ImageIds` | - | 皮肤图片资源 ID(优先级最高) | +| `color` | `CanvasStyle` | `"#333"` | 填充颜色(无皮肤时生效) | +| `border` | `CanvasStyle` | `"#666"` | 边框颜色(无皮肤时生效) | + +--- + +## 使用示例 + +### 图片皮肤模式 + +```tsx +// 使用预加载的UI背景图 + +``` + +### 纯色模式 + +```tsx +// 自定义颜色背景 + +``` + +### 对话框组合 + +```tsx +// 对话框容器 + + + + + +``` + +--- + +## 注意事项 + +1. **样式优先级** + 同时指定参数时的生效顺序: + + ```tsx + // 以下配置仅生效 winskin + + ``` + +2. **默认边框** + 未指定 border 时的行为: + + ```tsx + // 无边框(指定为透明色) + ; + + // 默认白色边框(当未指定任何参数时) + ; + ``` diff --git a/docs/api/user-client-modules/组件 Choices.md b/docs/api/user-client-modules/组件 Choices.md new file mode 100644 index 0000000..0de7953 --- /dev/null +++ b/docs/api/user-client-modules/组件 Choices.md @@ -0,0 +1,159 @@ +# Choices 选项框组件 API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +--- + +## 组件特性 + +- **多选一机制**:从多个选项中选择一项 +- **自动分页**:通过 `maxHeight` 控制分页 +- **灵活样式**:支持图片背景/纯色背景 + 自定义字体 +- **键盘导航**:方向键选择 + Enter 确认 +- **动态内容**:支持异步加载选项数据 + +--- + +## Props 属性说明 + +```mermaid +graph LR + ConfirmBoxProps --> TextContentProps + + click TextContentProps "./组件%20TextContent" +``` + +本组件完全继承 `TextContent` 组件的参数,参考 [组件 TextContent](./组件%20TextContent.md) + +| 属性名 | 类型 | 默认值 | 描述 | +| ----------- | ---------------- | -------- | ------------------------------------- | +| `choices` | `ChoiceItem[]` | 必填 | 选项数组,格式为 `[key, text]` 的元组 | +| `loc` | `ElementLocator` | 必填 | 定位配置(需包含 x,y 坐标及锚点) | +| `width` | `number` | 必填 | 选项框宽度(像素) | +| `maxHeight` | `number` | `360` | 最大高度(超过时自动分页) | +| `text` | `string` | - | 主说明文本(显示在标题下方) | +| `title` | `string` | - | 标题文本 | +| `winskin` | `ImageIds` | - | 背景图片资源 ID(与 color 互斥) | +| `color` | `CanvasStyle` | `#333` | 背景颜色(未设置 winskin 时生效) | +| `border` | `CanvasStyle` | `gold` | 边框颜色/样式 | +| `selFont` | `Font` | 系统默认 | 选项文本字体 | +| `selFill` | `CanvasStyle` | `#fff` | 选项文本颜色 | +| `titleFont` | `Font` | 系统默认 | 标题字体 | +| `titleFill` | `CanvasStyle` | `gold` | 标题颜色 | +| `interval` | `number` | `16` | 选项间垂直间距(像素) | + +--- + +## Events 事件说明 + +| 事件名 | 参数 | 触发时机 | +| -------- | ---------------- | ---------------------- | +| `choose` | `key: ChoiceKey` | 用户选择任意选项时触发 | + +--- + +## 使用示例 + +### 基础用法 - 系统设置 + +```tsx +import { defineComponent } from 'vue'; +import { Choices, ChoiceItem } from '@user/client-modules'; + +export const MyCom = defineComponent(() => { + const options: ChoiceItem[] = [ + ['low', '低画质'], + ['medium', '中画质'], + ['high', '高画质'] + ]; + + return () => ( + console.log(`Choose ${key}.`)} + /> + ); +}); +``` + +### 分页处理 - 角色选择 + +```tsx +import { defineComponent } from 'vue'; +import { Choices, ChoiceItem } from '@user/client-modules'; + +export const MyCom = defineComponent(() => { + // 生成 50 个角色选项 + const characters: ChoiceItem[] = Array.from( + { length: 50 }, + (_, i) => [`char_${i}`, `角色 ${i + 1}`] as ChoiceItem + ); + + return () => ( + + ); +}); +``` + +### 动态内容 + 自定义样式 + +```tsx +import { defineComponent } from 'vue'; +import { Choices, ChoiceItem } from '@user/client-modules'; +import { onTick } from '@motajs/render'; + +export const MyCom = defineComponent(() => { + const dynamicOptions = ref([]); + + onTick(() => { + // 每帧生成随机选项名称 + dynamicOptions.value = Array.from( + { length: 50 }, + (_, i) => + [ + `char_${i}`, + `随机数 ${Math.random().toFixed(8)}` + ] as ChoiceItem + ); + }); + + return () => ( + + ); +}); +``` + +--- + +## 注意事项 + +1. **选项键值唯一性** + 每个选项的 `key` 必须唯一,否则可能引发不可预期行为 + +2. **分页计算规则** + 分页依据 `maxHeight` 和字体大小自动计算,需确保字体大小一致 + +3. **使用更方便的函数**:多数情况下,你不需要使用本组件,使用包装好的函数往往会更加方便,参考 [`getChoice`](./functions.md#getchoice) diff --git a/docs/api/user-client-modules/组件 ConfirmBox.md b/docs/api/user-client-modules/组件 ConfirmBox.md new file mode 100644 index 0000000..31f5b4f --- /dev/null +++ b/docs/api/user-client-modules/组件 ConfirmBox.md @@ -0,0 +1,124 @@ +# ConfirmBox 确认框组件 API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +## 组件特性 + +- **双选项支持**:是/否选择 +- **灵活样式**:支持图片背景或纯色背景 +- **键盘交互**:支持按键操作 +- **自动布局**:根据内容动态计算高度 +- **事件驱动**:提供明确的用户选择反馈 + +--- + +## Props 属性说明 + +```mermaid +graph LR + ConfirmBoxProps --> TextContentProps + + click TextContentProps "./组件%20TextContent" +``` + +本组件完全继承 `TextContent` 组件的参数,参考 [组件 TextContent](./组件%20TextContent.md) + +| 属性名 | 类型 | 默认值 | 描述 | +| ------------ | ---------------- | ------------ | --------------------------------- | +| `text` | `string` | 必填 | 显示的主文本内容 | +| `width` | `number` | 必填 | 确认框宽度(像素) | +| `loc` | `ElementLocator` | 必填 | 定位配置 | +| `winskin` | `ImageIds` | - | 背景图片资源 ID(与 color 互斥) | +| `color` | `CanvasStyle` | `'#333'` | 背景颜色(未设置 winskin 时生效) | +| `border` | `CanvasStyle` | `'gold'` | 边框颜色/样式 | +| `selFont` | `Font` | 系统默认字体 | 选项按钮字体 | +| `selFill` | `CanvasStyle` | `'#d48'` | 选项按钮文本颜色 | +| `yesText` | `string` | `'是'` | 确认按钮文本 | +| `noText` | `string` | `'否'` | 取消按钮文本 | +| `defaultYes` | `boolean` | `true` | 默认选中确认按钮 | + +--- + +## Events 事件说明 + +| 事件名 | 参数 | 触发时机 | +| ------ | ---- | --------------------------- | +| `yes` | - | 用户选择确认时触发 | +| `no` | - | 用户选择取消或按 Esc 时触发 | + +--- + +## 使用示例 + +### 基础用法 - 文本确认 + +```tsx +import { defineComponent } from 'vue'; +import { ConfirmBox } from '@user/client-modules'; + +export const MyCom defineComponent(() => { + return () => ( + console.log('用户确认保存')} + onNo={() => console.log('用户取消保存')} + /> + ); +}); +``` + +### 图片背景 + 自定义按钮 + +```tsx +import { defineComponent } from 'vue'; +import { ConfirmBox } from '@user/client-modules'; +import { Font } from '@motajs/render'; + +export const MyCom = defineComponent(() => { + return () => ( + + ); +}); +``` + +### 动态内容 + 编程控制 + +```tsx +import { defineComponent, computed } from 'vue'; +import { ConfirmBox } from '@user/client-modules'; + +export const MyCom = defineComponent(() => { + const count = ref(0); + const myText = computed(() => `当前确认次数与取消次数差值:${count.value}`); + + return () => ( + void count.value++} // 每次确认次数加一 + onNo={() => void count.value--} // 每次确认次数减一 + /> + ); +}); +``` + +## 注意事项 + +1. **使用更方便的函数**:多数情况下,你不需要使用本组件,使用包装好的函数往往会更加方便,参考 [`getConfirm`](./functions.md#getconfirm) diff --git a/docs/api/user-client-modules/组件 Page.md b/docs/api/user-client-modules/组件 Page.md new file mode 100644 index 0000000..01c0b4f --- /dev/null +++ b/docs/api/user-client-modules/组件 Page.md @@ -0,0 +1,192 @@ +# Page 分页组件 API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +## 组件描述 + +分页组件用于将大量内容分割为多个独立页面展示,支持通过编程控制或用户交互进行页面切换。适用于存档界面、多步骤表单等场景。 + +--- + +## Props 属性说明 + +| 属性名 | 类型 | 默认值 | 描述 | +| -------------- | ---------------- | ----------------- | -------------------------------- | +| `pages` | `number` | 必填 | 总页数 | +| `loc` | `ElementLocator` | 必填 | 页码组件定位配置(坐标系及位置) | +| `font` | `Font` | `Font.defaults()` | 页码文本字体配置(可选) | +| `hideIfSingle` | `boolean` | `false` | 当总页数为 1 时是否隐藏页码组件 | + +--- + +## Events 事件说明 + +| 事件名 | 参数类型 | 触发时机 | +| ------------ | ---------------- | ------------------------------- | +| `pageChange` | `(page: number)` | 当前页码变化时触发(从 0 开始) | + +--- + +## Slots 插槽说明 + +### `default` + +接收当前页码(从 0 开始)并返回需要渲染的内容 +**参数** + +- `page: number` 当前页码索引(0-based) + +--- + +## Exposed Methods 暴露方法 + +| 方法名 | 参数 | 返回值 | 描述 | +| ------------ | --------------- | -------- | --------------------------------------------------- | +| `changePage` | `page: number` | `void` | 跳转到指定页码(0-based,自动约束在有效范围内) | +| `movePage` | `delta: number` | `void` | 基于当前页码进行偏移切换(如 +1 下一页,-1 上一页) | +| `now` | - | `number` | 获取当前页码索引(0-based) | + +--- + +## 使用示例 + +### 基础用法 - 多页文本展示 + +```tsx +import { defineComponent } from 'vue'; +import { Page, PageExpose } from '@user/client-modules'; + +export const MyCom = defineComponent(() => { + return () => ( + + {page => ( + + )} + + ); +}); +``` + +### 监听页面修改 + +```tsx +import { defineComponent, ref } from 'vue'; +import { Page, PageExpose } from '@user/client-modules'; + +export const MyCom = defineComponent(() => { + // 示例数据 + const pages = [ + { content: '第一页内容' }, + { content: '第二页内容' }, + { content: '第三页内容' } + ]; + + // 分页组件引用 + const pageRef = ref(); + + // 页码变化回调 + const handlePageChange = (currentPage: number) => { + // 可以使用参数获得当前页码,加一是因为页码是从 0 开始的 + console.log(`当前页码:${currentPage + 1}`); + // 或者也可以使用 Page 组件的接口获得当前页码 + console.log(`当前页码:${pageRef.value!.now() + 1}`); + }; + + return () => ( + + {page => } + + ); +}); +``` + +### 动态配置示例 + +```tsx +import { defineComponent, ref } from 'vue'; +import { Page, PageExpose } from '@user/client-modules'; + +// 带统计面板的复杂分页 +export const MyCom = defineComponent(() => { + const dataPages = [ + /* 复杂数据结构 */ + ]; + + // 暴露方法实现翻页逻辑 + const pageRef = ref(); + const jumpToAnalysis = () => pageRef.value?.changePage(3); // 1-based + + return () => ( + + {/* 分页内容 */} + + {page => ( + + {/* 这里面可以写一些复杂的渲染内容,或者单独写成一个组件,把页码作为参数传入 */} + + + + )} + + + {/* 自定义跳转按钮 */} + + + ); +}); +``` + +### 边缘检测示例 + +```tsx +import { defineComponent, ref } from 'vue'; + +// 边界处理逻辑 +export const MyCom = defineComponent(() => { + const pageRef = ref(); + + // 自定义边界提示 + const handleEdge = () => { + const current = pageRef.value?.now() ?? 0; + const total = pageRef.value?.pages ?? 0; + + // 到达边界时提示(可以换成其他提示方式) + if (current === 0) core.drawTip('已经是第一页'); + if (current === total - 1) core.drawTip('已经是最后一页'); + }; + + return () => ( + + {page => } + + ); +}); +``` + +--- + +## 注意事项 + +1. **自动约束**:切换页码时会自动约束在 `[0, pages-1]` 范围内 diff --git a/docs/api/user-client-modules/组件 Progress.md b/docs/api/user-client-modules/组件 Progress.md new file mode 100644 index 0000000..322da96 --- /dev/null +++ b/docs/api/user-client-modules/组件 Progress.md @@ -0,0 +1,107 @@ +# Progress 组件 API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +--- + +## 核心特性 + +- **动态进度显示**:支持 0.0~1.0 范围的进度可视化 +- **双色样式分离**:可分别定制已完成/未完成部分样式 +- **精准定位**:支持像素级坐标控制 +- **平滑过渡**:数值变化自动触发重绘 + +--- + +## Props 属性说明 + +| 属性名 | 类型 | 默认值 | 描述 | +| ------------ | ---------------- | -------- | --------------------- | +| `loc` | `ElementLocator` | **必填** | 进度条容器坐标 | +| `progress` | `number` | **必填** | 当前进度值(0.0~1.0) | +| `success` | `CanvasStyle` | `green` | 已完成部分填充样式 | +| `background` | `CanvasStyle` | `gray` | 未完成部分填充样式 | +| `lineWidth` | `number` | `2` | 进度条线宽(像素) | + +--- + +## 使用示例 + +### 基础用法 + +```tsx +import { defineComponent, ref } from 'vue'; +import { Progress } from '@user/client-modules'; +import { onTick } from '@motajs/render'; + +export const MyCom = defineComponent(() => { + // 创建响应式进度值 + const loadingProgress = ref(0); + + // 模拟进度更新 + onTick(() => { + if (loadingProgress.value < 1) { + loadingProgress.value += 0.002; + } + }); + + return () => ( + + ); +}); +``` + +### 自定义样式 + +```tsx +// 自定义进度条的已完成和未完成部分的样式 + +``` + +### 垂直进度条 + +```tsx +// 通过旋转容器实现垂直效果,注意锚点的使用 + + + +``` + +--- + +## 动画效果实现 + +### 平滑过渡示例 + +```tsx +import { transitioned } from '@user/client-modules'; +import { pow } from 'mutate-animate'; + +const progressValue = transitioned(0, 2000, pow(2, 'out')); +progressValue.set(1); // 2秒内完成二次曲线平滑过渡 + +return () => ( + +); +``` + +--- + +## 注意事项 + +1. **坐标系统** + 实际渲染高度由 `loc[3]` 参数控制,会自动上下居中: + + ```tsx + // 情况1:显式指定高度为8px + + ``` diff --git a/docs/api/user-client-modules/组件 Scroll.md b/docs/api/user-client-modules/组件 Scroll.md new file mode 100644 index 0000000..29ae75f --- /dev/null +++ b/docs/api/user-client-modules/组件 Scroll.md @@ -0,0 +1,118 @@ +# Scroll 滚动组件 API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +## 组件特性 + +- **虚拟滚动**:自动裁剪可视区域外元素 +- **双模式支持**:垂直/水平滚动(默认垂直) +- **性能优化**:动态计算可视区域,支持万级元素流畅滚动 +- **编程控制**:支持精准定位滚动位置 + +--- + +## Props 属性说明 + +| 属性名 | 类型 | 默认值 | 描述 | +| ---------- | ---------------- | ------- | ------------------------------------------------- | +| `hor` | `boolean` | `false` | 启用水平滚动模式 | +| `noscroll` | `boolean` | `false` | 是否不显示滚动条,可用于一些特殊场景 | +| `loc` | `ElementLocator` | 必填 | 滚动容器定位配置 | +| `padEnd` | `number` | `0` | 滚动到底部/右侧的额外留白(用于修正自动计算误差) | + +--- + +## Exposed Methods 暴露方法 + +| 方法名 | 参数 | 返回值 | 描述 | +| ----------------- | --------------------------------- | -------- | --------------------------------------------------- | +| `scrollTo` | `position: number, time?: number` | `void` | 滚动到指定位置(单位:像素),time 为过渡时间(ms) | +| `getScrollLength` | - | `number` | 获取最大可滚动距离(单位:像素) | + +--- + +## Slots 插槽说明 + +### `default` + +接收滚动内容,**必须直接包含可渲染元素** +⚠️ 禁止嵌套 container 包裹,推荐平铺结构: + +```tsx +// ✅ 正确写法 + + + + +; + +// ❌ 错误写法(影响虚拟滚动计算) + + + // 会导致整体被视为单个元素 + + + +; +``` + +--- + +## 使用示例 + +### 基础垂直滚动 + +```tsx +import { defineComponent } from 'vue'; + +export const MyCom = defineComponent(() => { + const list = Array(200).fill(0); + + return () => ( + + {list.map((_, index) => ( + + ))} + + ); +}); +``` + +### 水平滚动 + 编程控制 + +```tsx +import { defineComponent, ref, onMounted } from 'vue'; + +export const MyCom = defineComponent(() => { + const list = Array(200).fill(0); + const scrollRef = ref(); + + // 滚动水平 100 像素位置,动画时长 500 毫秒 + onMounted(() => { + scrollRef.value?.scrollTo(100, 500); + }); + + return () => ( + + {list.map((_, index) => ( + + ))} + + ); +}); +``` + +--- + +## 性能优化指南 + +### 1. 替代方案建议 + +⚠️ **当子元素数量 > 1000 时**,推荐改用分页组件: + +```tsx +// 使用 Page 组件处理超大数据集 + + {page => renderChunk(data.slice(page * 50, (page + 1) * 50))} + +``` diff --git a/docs/api/user-client-modules/组件 ScrollText.md b/docs/api/user-client-modules/组件 ScrollText.md new file mode 100644 index 0000000..cc9e7ea --- /dev/null +++ b/docs/api/user-client-modules/组件 ScrollText.md @@ -0,0 +1,160 @@ +# ScrollText 组件 API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +--- + +## 核心特性 + +- **自动滚动**:支持设定滚动速度的纵向滚动效果 +- **长文本支持**:内置高性能文本渲染引擎 +- **精准控制**:提供播放/暂停/调速等操作接口 +- **智能布局**:自动计算文本高度和滚动距离 + +--- + +## Props 属性说明 + +```mermaid +graph LR + ScrollTextProps --> TextContentProps + ScrollTextProps --> ScrollProps + + click TextContentProps "./组件%20TextContent" + click ScrollProps "./组件%20Scroll" +``` + +完全继承 `TextContent` 组件和 `Scroll` 组件的参数和事件。 + +| 属性名 | 类型 | 默认值 | 描述 | +| ------- | ---------------- | ------ | --------------------------- | +| `speed` | `number` | 必填 | 滚动速度(像素/秒) | +| `width` | `number` | 必填 | 文本区域固定宽度(像素) | +| `loc` | `ElementLocator` | 必填 | 容器定位 [x,y,width,height] | +| `pad` | `number` | `16` | 首行前空白距离(像素) | + +--- + +## 事件说明 + +| 事件名 | 参数 | 触发时机 | +| ----------- | ---- | -------------------- | +| `scrollEnd` | - | 滚动到文本末尾时触发 | + +--- + +## Exposed Methods 暴露方法 + +| 方法名 | 参数 | 返回值 | 描述 | +| ---------- | --------------- | ------ | --------------------------- | +| `pause` | - | void | 暂停滚动 | +| `resume` | - | void | 继续滚动 | +| `setSpeed` | `speed: number` | void | 动态调整滚动速度(像素/秒) | +| `rescroll` | - | void | 立即重置到起始位置重新滚动 | + +--- + +## 使用示例 + +### 基础滚动 + +```tsx +import { defineComponent } from 'vue'; +import { ScrollText } from '@user/client-modules'; + +export const MyCom = defineComponent(() => { + const longText = '序幕\n'.repeat(100) + '——全剧终——'; + + return () => ( + + ); +}); +``` + +### 动态控制 + +```tsx +import { defineComponent, ref } from 'vue'; +import { ScrollText } from '@user/client-modules'; + +export const MyCom = defineComponent(() => { + const longText = '序幕\n'.repeat(100) + '——全剧终——'; + const scrollRef = ref(); + + // 暂停/恢复控制 + const toggleScroll = () => { + if (scrollRef.value?.isPaused) { + scrollRef.value.resume(); + } else { + scrollRef.value?.pause(); + } + }; + + // 速度控制 + const accelerate = () => { + scrollRef.value?.setSpeed(200); + }; + + return () => ( + console.log('滚动结束')} + /> + ); +}); +``` + +### 复杂排版 + +```tsx +const staffText = + '\\c[32]====制作人员====\\c\n\n' + + '\\r[#FFD700]总监督\\r\t\t张三\n' + + '\\r[#00FF00]美术指导\\r\\t李四\n' + + '\\i[logo]\n' + + '特别感谢:某某公司'; + +; +``` + +--- + +## 注意事项 + +1. **容器尺寸** + 实际可滚动区域计算公式: + + ``` + 可视高度 = loc[3](容器高度) + 滚动距离 = 文本总高度 + pad(首行前空白) + ``` + +2. **速度控制** + 推荐速度范围 50-200 像素/秒 + +3. **组合动画** + 可与容器变换配合实现复杂效果: + ```tsx + + + + ``` diff --git a/docs/api/user-client-modules/组件 Selection.md b/docs/api/user-client-modules/组件 Selection.md new file mode 100644 index 0000000..3a86afd --- /dev/null +++ b/docs/api/user-client-modules/组件 Selection.md @@ -0,0 +1,77 @@ +# Selection 组件 API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +--- + +## 核心特性 + +- **动态高亮**:自动呼吸动画效果 +- **双样式模式**:支持图片皮肤或纯色样式 +- **精准定位**:像素级坐标控制 +- **透明度动画**:可定制不透明度变化范围 + +--- + +## Props 属性说明 + +| 属性名 | 类型 | 默认值 | 描述 | +| ------------ | ------------------ | -------------- | --------------------------------- | +| `loc` | `ElementLocator` | **必填** | 光标定位 | +| `winskin` | `ImageIds` | - | 图片资源 ID(优先级最高) | +| `color` | `CanvasStyle` | `#ddd` | 填充颜色(无皮肤时生效) | +| `border` | `CanvasStyle` | `gold` | 边框颜色(无皮肤时生效) | +| `alphaRange` | `[number, number]` | `[0.25, 0.55]` | 不透明度波动范围 [最小值, 最大值] | + +--- + +## 使用示例 + +### 图片皮肤模式 + +```tsx +// 使用预加载的游戏皮肤资源 + +``` + +--- + +### 纯色模式 + +```tsx +// 自定义颜色方案 + +``` + +--- + +## 注意事项 + +1. **样式优先级** + 同时指定 `winskin` 和颜色参数时: + + ```tsx + // 以下配置将忽略 color/border 参数 + + ``` + +2. **动画速度** + 呼吸动画固定为 2000ms/周期,暂不支持自定义时长 + +3. **点击反馈** + 建议配合事件系统实现点击效果: + ```tsx + + + + + ``` diff --git a/docs/api/user-client-modules/组件 TextContent.md b/docs/api/user-client-modules/组件 TextContent.md new file mode 100644 index 0000000..1dbe2c4 --- /dev/null +++ b/docs/api/user-client-modules/组件 TextContent.md @@ -0,0 +1,184 @@ +# TextContent 文本组件 API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +--- + +## 核心特性 + +- **自动布局**:根据宽度自动换行 +- **样式控制**:支持动态修改字体/颜色/描边 +- **打字机效果**:逐字显示支持 +- **动态高度**:自适应或固定高度模式 + +--- + +## Props 属性说明 + +| 属性名 | 类型 | 默认值 | 描述 | +| ----------------- | ------------- | ----------------- | ------------------------------ | +| `text` | `string` | - | 显示文本(支持转义字符) | +| `width` | `number` | 必填 | 文本区域宽度(像素) | +| `fill` | `boolean` | `true` | 是否对文字填充 | +| `stroke` | `boolean` | `false` | 是否对文字描边 | +| `font` | `Font` | 系统默认字体 | 字体配置对象 | +| `lineHeight` | `number` | `0` | 行间距(像素) | +| `interval` | `number` | `50` | 打字机字符间隔(ms) | +| `autoHeight` | `boolean` | `false` | 是否根据内容自动调整高度 | +| `fillStyle` | `CanvasStyle` | `#fff` | 文字填充颜色 | +| `strokeStyle` | `CanvasStyle` | `#000` | 文字描边颜色 | +| `strokeWidth` | `number` | `1` | 描边宽度 | +| `textAlign` | `TextAlign` | `TextAlign.Left` | 文字对齐方式 | +| `wordBreak` | `WordBreak` | `WordBreak.Space` | 文本分词原则,将会影响换行表现 | +| `breakChars` | `string` | - | 会被分词规则识别的分词字符 | +| `ignoreLineStart` | `string` | - | 不允许出现在行首的字符 | +| `ignoreLineEnd` | `string` | - | 不允许出现在行尾的字符 | + +--- + +## 事件说明 + +| 事件名 | 参数 | 触发时机 | +| -------------- | ---------------- | ------------------ | +| `typeStart` | - | 开始逐字显示时 | +| `typeEnd` | - | 全部文字显示完成时 | +| `updateHeight` | `height: number` | 文本高度变化时 | + +--- + +## Exposed Methods 暴露方法 + +| 方法名 | 参数 | 返回值 | 描述 | +| ----------- | ---- | -------- | ---------------------------- | +| `retype` | - | `void` | 从头开始重新打字 | +| `showAll` | - | `void` | 立刻结束打字机,显示所有文字 | +| `getHeight` | - | `number` | 获得当前文本的高度 | + +--- + +## 使用示例 + +### 基础用法 - 对话文本 + +```tsx +import { defineComponent } from 'vue'; +import { TextContent } from '@user/client-modules'; +import { Font } from '@motajs/render'; + +export const MyCom = defineComponent(() => { + return () => ( + console.log('显示完成')} // 打字机结束后执行 + /> + ); +}); +``` + +### 自定义样式 + 描边效果 + +```tsx +import { defineComponent } from 'vue'; +import { TextContent } from '@user/client-modules'; +import { Font } from '@motajs/render'; + +export const MyCom = defineComponent(() => { + return () => ( + + ); +}); +``` + +### 动态内容更新 + +```tsx +import { defineComponent, ref } from 'vue'; +import { TextContent } from '@user/client-modules'; + +export const MyCom = defineComponent(() => { + const dynamicText = ref('初始内容'); + + setTimeout(() => { + dynamicText.value = '更新后的内容\\z[5]带暂停效果'; + }, 2000); + + return () => ( + console.log('当前高度:', h)} // 当高度发生变化时触发 + /> + ); +}); +``` + +### 禁用动画效果 + +```tsx +import { defineComponent } from 'vue'; +import { TextContent } from '@user/client-modules'; + +export const MyCom = defineComponent(() => { + return () => ( + + ); +}); +``` + +### 多语言复杂排版 + +```tsx +import { defineComponent } from 'vue'; +import { TextContent } from '@user/client-modules'; + +export const MyCom = defineComponent(() => { + const complexText = + '\\g[Times New Roman]Hello\\g[宋体] 你好 \\i[flag]\\n' + + '\\r[#FF5733]Multi\\r[#3498db]-\\r[#2ECC71]Color\\r\\n' + + '\\c[18]Small\\c[24]Size\\c[30]Changes'; + + return () => ( + console.log('开始渲染复杂文本')} + /> + ); +}); +``` + +--- + +## 转义字符示例 + +```tsx +// 颜色/字体/图标综合使用 +const styledText = + '\\r[#FF0000]警告!\\g[方正粗宋]\\c[24]' + + '\\i[warning_icon]发现异常\\z[10]\\n' + + '请立即处理\\r\\g\\c'; + +; +``` + +转义字符具体用法参考 [TextContentParser](./TextContentParser.md#转义字符语法说明) diff --git a/docs/api/user-client-modules/组件 Textbox.md b/docs/api/user-client-modules/组件 Textbox.md new file mode 100644 index 0000000..c0a2e44 --- /dev/null +++ b/docs/api/user-client-modules/组件 Textbox.md @@ -0,0 +1,173 @@ +# Textbox 对话框组件 API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +本文档描述在 `TextContent` 基础上扩展的对话框组件,专为剧情对话、系统提示等场景设计,支持背景、标题等装饰元素。 + +--- + +## 特有属性说明 + +```mermaid +graph LR + TextboxProps --> TextContentProps + + click TextContentProps "./组件%20TextContent" +``` + +| 属性名 | 类型 | 默认值 | 描述 | +| -------------- | ------------- | -------------- | ------------------------------------- | +| `backColor` | `CanvasStyle` | `#222` | 背景颜色(与 `winskin` 互斥) | +| `winskin` | `ImageIds` | - | 背景图片资源 ID(优先于 `backColor`) | +| `padding` | `number` | `8` | 内容区域与边框的内边距(像素) | +| `title` | `string` | - | 标题文本内容 | +| `titleFont` | `Font` | `18px Verdana` | 标题字体配置 | +| `titleFill` | `CanvasStyle` | `gold` | 标题文字颜色 | +| `titleStroke` | `CanvasStyle` | `transparent` | 标题描边颜色 | +| `titlePadding` | `number` | `8` | 标题在其背景的间距(像素) | + +--- + +## 事件说明 + +```mermaid +graph LR + TextboxEmits --> TextContentEmits + + click TextContentEmits "./组件%20TextContent" +``` + +完全继承 `TextContent` 的事件。 + +--- + +## Exposed Methods 暴露方法 + +| 方法名 | 参数 | 返回值 | 描述 | +| --------- | ---- | ------ | ---------------------------- | +| `retype` | - | `void` | 从头开始重新打字 | +| `showAll` | - | `void` | 立刻结束打字机,显示所有文字 | +| `show` | - | `void` | 显示这个文本框 | +| `hide` | - | `void` | 隐藏这个文本框 | + +--- + +## Slots 插槽说明 + +### default + +背景插槽,传入后可以自定义背景 + +```tsx +// 例如使用一张图片作为背景 + + + + + +``` + +### title + +标题背景插槽,自定义标题背景 + +```tsx +// 与 default 一起使用 + + {{ + // 背景图 + default: () => , + // 标题背景图 + title: () => + }} + +``` + +## 使用示例 + +### 基础对话框 + +```tsx +import { defineComponent } from 'vue'; +import { Textbox } from '@user/client-modules'; + +export const MyCom = defineComponent(() => { + return () => ( + + ); +}); +``` + +### 纯色背景 + 复杂标题 + +```tsx +import { defineComponent } from 'vue'; +import { Textbox } from '@user/client-modules'; + +export const MyCom = defineComponent(() => { + return () => ( + + ); +}); +``` + +### 动态标题交互 + +```tsx +import { defineComponent } from 'vue'; +import { Textbox, TextbosExpose } from '@user/client-modules'; + +export const MyCom = defineComponent(() => { + const currentTitle = ref(); + + // 点击按钮切换标题 + const toggleTitle = () => { + currentTitle.value += 1; + }; + + return () => ( + + + + + ); +}); +``` + +--- + +## 布局结构示意图 + +```mermaid +graph TB + Dialog[对话框] --> Background[背景层] + Dialog --> Title[标题层] + Dialog --> Content[内容层] + + Background -->|winskin/backColor| 渲染背景 + Title -->|title 配置| 标题文本 + Content -->|padding 控制| 文字内容 +``` diff --git a/docs/api/user-client-modules/组件 Tip.md b/docs/api/user-client-modules/组件 Tip.md new file mode 100644 index 0000000..1eae585 --- /dev/null +++ b/docs/api/user-client-modules/组件 Tip.md @@ -0,0 +1,65 @@ +# Tip 组件 API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +## 参数说明(Props) + +| 参数名 | 类型 | 默认值 | 说明 | +| -------- | ------------------ | -------- | ---------------------------------------- | +| `loc` | `ElementLocator` | 必填 | 容器基础定位参数 [x,y,width,height] | +| `pad` | `[number, number]` | `[4,4]` | 图标与文字的边距配置 [水平边距,垂直边距] | +| `corner` | `number` | `4` | 圆角矩形的圆角半径 | +| `id` | `string` | 自动生成 | 提示框唯一标识(需全局唯一) | + +## 暴露接口(Expose) + +| 方法名 | 参数 | 返回值 | 说明 | +| --------- | ----------------------------------------------- | ------ | --------------------------------------------------- | +| `drawTip` | `text: string``icon?: AllIds \| AllNumbers` | `void` | 显示带图标的提示文本(图标支持字符串 ID 或数字 ID) | + +## 使用示例 + +```tsx +import { defineComponent } from 'vue'; +import { Tip } from './tip'; + +// 在游戏界面中定义提示组件 +export const MyCom = defineComponent(() => { + return () => ( + + + + ); +}); + +// 在业务代码中调用提示,使用 TipStore 类 +const tip = TipStore.get('global-tip'); +tip?.drawTip('宝箱已解锁!', 'chest_icon'); +``` + +## 特性说明 + +1. **自动布局**: + + - 根据图标尺寸自动计算容器宽度 + - 文字垂直居中显示 + - 图标与文字间距自动适配 + +2. **动画效果**: + + - 默认带有 500ms 双曲正弦缓动的淡入动画 + - 3 秒无操作后自动淡出 + - 支持通过`alpha`参数自定义过渡效果 + +## 注意事项 + +1. **全局单例**:建议通过 `TipStore` 进行全局管理,避免重复创建 +2. **ID 唯一性**:未指定 `id` 时会自动生成格式为 `@default-tip-数字` 的标识 +3. **自动隐藏**:调用 `drawTip` 后 3 秒自动隐藏,连续调用会重置计时 +4. **性能优化**:使用 `lodash` 的 `debounce` 进行隐藏操作防抖 +5. **动画配置**:可通过修改 `hyper('sin', 'in-out')` 参数调整动画曲线 diff --git a/docs/api/user-client-modules/组件 Waitbox.md b/docs/api/user-client-modules/组件 Waitbox.md new file mode 100644 index 0000000..9cd2d94 --- /dev/null +++ b/docs/api/user-client-modules/组件 Waitbox.md @@ -0,0 +1,104 @@ +# WaitBox 等待框组件 API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +--- + +## 核心特性 + +- **Promise 绑定**:自动监控 `Promise` 状态 +- **复合式组件**:集成背景+文字+加载动画 +- **双重控制**:支持自动/手动完成等待 +- **动态布局**:根据内容自动计算高度 + +--- + +## 组件定位 + +> 💡 更推荐使用 `waitbox` 工具函数,该组件主要用于需要深度定制的场景。参考 [此文档](./functions.md#waitbox)。 + +--- + +## Props 属性说明 + +| 属性名 | 类型 | 必填 | 描述 | +| --------- | ---------------- | ---- | ----------------------- | +| `promise` | `Promise` | 否 | 要监控的 `Promise` 对象 | +| `loc` | `ElementLocator` | 是 | 容器定位 | +| `width` | `number` | 是 | 内容区域宽度(像素) | +| `text` | `string` | 否 | 等待提示文字 | +| `pad` | `number` | `16` | 文字与边缘间距 | + +### 继承属性 + +- 支持所有 `Background` 背景属性 +- 支持所有 `TextContent` 文本属性 + +--- + +## 事件说明 + +| 事件名 | 参数 | 触发时机 | +| --------- | ---- | -------------------------- | +| `resolve` | `T` | `Promise` 完成时返回结果值 | + +--- + +## Exposed Methods 暴露方法 + +| 方法名 | 参数 | 描述 | +| --------- | --------- | ---------------------------- | +| `resolve` | `data: T` | 手动完成等待(立即触发事件) | + +--- + +## 使用示例 + +### 基础组件用法 + +```tsx +// 等待网络请求 +const fetchPromise = fetchData(); + + console.log('收到数据:', data)} +/>; +``` + +### 手动控制示例 + +```tsx +const waitRef = ref>(); + +// 手动结束等待 +const forceComplete = () => { + waitRef.value?.resolve(Date.now()); +}; + +return () => ( + +); +``` + +--- + +## 注意事项 + +1. **推荐用法** + 90% 场景应使用 `waitbox` 函数,以下情况才需要直接使用组件: + + - 需要永久显示的等待界面 + - 需要组合复杂子组件 + - 需要复用同一个等待实例 diff --git a/docs/api/user-data-base/index.md b/docs/api/user-data-base/index.md new file mode 100644 index 0000000..ae599ee --- /dev/null +++ b/docs/api/user-data-base/index.md @@ -0,0 +1,3 @@ +# @user/data-base + +目录: diff --git a/docs/api/user-data-base/钩子.md b/docs/api/user-data-base/钩子.md new file mode 100644 index 0000000..4fc989b --- /dev/null +++ b/docs/api/user-data-base/钩子.md @@ -0,0 +1,97 @@ +# 游戏事件钩子 API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +--- + +## GameLoading 加载进度钩子 + +### 核心事件说明 + +| 事件名 | 触发时机 | 参数 | +| ------------------ | -------------------------- | ---- | +| `coreLoaded` | 核心脚本加载完成时 | 无 | +| `autotileLoaded` | 所有自动元件资源加载完成时 | 无 | +| `coreInit` | 核心类初始化完成时 | 无 | +| `loaded` | 所有启动必要资源加载完成时 | 无 | +| `registered` | 客户端和数据端都完成挂载时 | 无 | +| `dataRegistered` | 数据端服务挂载完成时 | 无 | +| `clientRegistered` | 渲染端挂载完成时 | 无 | + +--- + +### 使用示例 + +```typescript +import { loading } from '@user/data-base'; + +// 监听核心初始化事件 +loading.on('coreInit', () => { + console.log('核心系统已就绪'); + initializeCustomModules(); +}); + +// 监听完整加载事件 +loading.once('loaded', () => { + showMainMenu(); + preloadOptionalAssets(); +}); +``` + +--- + +## GameEvent 游戏运行时钩子 + +### 核心事件说明 + +| 事件名 | 触发时机 | 参数 | +| ------------------ | ------------------------------------ | ------------------------------------------------------------------------------- | +| `reset` | 游戏初始化时,例如读档后、进入游戏后 | 无 | +| `mounted` | 游戏 DOM 挂载完成后 | 无 | +| `statusBarUpdate` | 状态栏更新时 | 无 | +| `renderLoaded` | 渲染端加载完成时 | 无 | +| `afterGetItem` | 拾取道具后 | `[itemId: 道具ID, x: 坐标X, y: 坐标Y, isGentleClick: 是否轻击]` | +| `afterOpenDoor` | 成功开门后 | `[doorId: 门动画ID, x: 坐标X, y: 坐标Y]` | +| `afterChangeFloor` | 楼层切换完成后 | `[floorId: 新楼层ID]` | +| `moveOneStep` | 玩家移动一步后 | `[x: 新坐标X, y: 新坐标Y, floorId: 当前楼层ID]` | +| `afterBattle` | 战斗结算完成后 | `[enemy: 敌人数据对象, x?: 战斗坐标X, y?: 战斗坐标Y]` | +| `changingFloor` | 楼层切换过程中(动画播放时) | `[floorId: 目标楼层ID, heroLoc: 玩家位置对象]` | +| `setBlock` | 地图图块被修改时 | `[x: 坐标X, y: 坐标Y, floorId: 楼层ID, newBlock: 新图块值, oldBlock: 旧图块值]` | +| `enemyExtract` | 解析敌人数据时 | `[col: 敌人集合对象]` | +| `restart` | 从游戏返回标题界面时 | 无 | +| `setBgFgBlock` | 设置背景/前景图块时 | `[name: 图层名称, number: 图块值, x: 坐标X, y: 坐标Y, floorId: 楼层ID]` | +| `replayStatus` | 录像播放状态切换时 | `[replaying: 是否正在回放]` | +| `loadData` | 加载存档时 | 无 | + +--- + +### 使用示例 + +```typescript +// 监听玩家移动事件 +hook.on('moveOneStep', (x, y, floorId) => { + console.log(x, y, floorId); +}); + +// 监听战斗结束事件 +hook.on('afterBattle', (enemy, x, y) => { + console.log(enemy, x, y); +}); + +// 监听存档加载事件 +hook.once('loadData', () => { + console.log('读档成功!'); +}); +``` + +--- + +## 弃用说明 + +```typescript +/** + * @deprecated 自 v2.B 起废弃的 GameListener + * 计划在 v2.C 移除,请使用新的 UI 交互系统代替 + */ +export const gameListener = new GameListener(); +``` diff --git a/docs/api/user-data-fallback/index.md b/docs/api/user-data-fallback/index.md new file mode 100644 index 0000000..259983b --- /dev/null +++ b/docs/api/user-data-fallback/index.md @@ -0,0 +1,3 @@ +# @user/data-fallback + +目录: diff --git a/docs/api/user-data-state/ObjectMoverBase.md b/docs/api/user-data-state/ObjectMoverBase.md new file mode 100644 index 0000000..09400bd --- /dev/null +++ b/docs/api/user-data-state/ObjectMoverBase.md @@ -0,0 +1,424 @@ +# ObjectMoverBase API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +--- + +## 类描述 + +游戏中可移动对象的基类控制器,提供面向方向、移动队列管理和动画协调的通用移动能力。继承自 EventEmitter3,用于实现图块、角色等元素的移动控制。 + +```mermaid +graph LR + ObjectMoverBase --> EventEmitter + + click EventEmitter "https://nodejs.org/api/events.html#class-eventemitter" +``` + +--- + +## 核心属性 + +| 属性名 | 类型 | 说明 | +| ------------ | ----------------- | ------------------------ | +| `moveSpeed` | `number` | 当前移动速度(毫秒/格) | +| `moveDir` | `Dir2` | 当前移动方向(八方向) | +| `moving` | `boolean` | 是否处于移动状态 | +| `faceDir` | `Dir2` | 当前面朝方向(八方向) | +| `controller` | `IMoveController` | 当前移动控制实例(只读) | + +--- + +## 事件说明 + +| 事件名 | 参数 | 触发时机 | +| ----------- | ------------- | ------------------ | +| `stepEnd` | `MoveStepDir` | 单步移动完成时 | +| `moveEnd` | - | 整个移动队列完成时 | +| `moveStart` | `MoveStep[]` | 移动队列开始执行时 | + +--- + +## 方法说明 + +### `startMove` + +```typescript +function startMove(): IMoveController | null; +``` + +**功能** +启动移动队列执行 + +**返回值** +`IMoveController`:移动控制器实例(可追加指令) +`null`:队列为空或已在移动中时返回 + +**示例** + +```typescript +const controller = mover.startMove(); +if (controller) { + controller.push({ type: 'dir', value: 'right' }); +} +``` + +--- + +### `insertMove` + +```typescript +function insertMove(...move: MoveStep[]): void; +``` + +| 参数 | 类型 | 说明 | +| ------ | ------------ | ------------ | +| `move` | `MoveStep[]` | 移动指令序列 | + +**功能** +向队列末尾插入移动指令 + +**示例** + +```typescript +// 添加转向+加速指令 +mover.insertMove({ type: 'dir', value: 'left' }, { type: 'speed', value: 200 }); +``` + +--- + +### `clearMoveQueue` + +```typescript +function clearMoveQueue(): void; +``` + +**功能** +清空所有待执行移动指令 + +**注意** +不影响已开始的移动步骤 + +--- + +### `oneStep` + +```typescript +function oneStep(step: Move2): void; +``` + +| 参数 | 类型 | 说明 | +| ------ | ------- | --------------------------------- | +| `step` | `Move2` | 移动方向(支持八向/前后相对方向) | + +**功能** +添加单步方向移动指令 + +**示例** + +```typescript +// 添加面朝方向移动指令 +mover.oneStep('forward'); +``` + +--- + +### `moveAs` + +```typescript +function moveAs(steps: MoveStep[]): void; +``` + +| 参数 | 类型 | 说明 | +| ------- | ------------ | ------------------ | +| `steps` | `MoveStep[]` | 结构化移动指令序列 | + +**功能** +批量加载复杂移动路径 + +**示例** + +```typescript +mover.moveAs([ + { type: 'dir', value: 'up' }, // 向上移动 + { type: 'speed', value: 150 }, // 修改速度为每 150ms 移动一格 + { type: 'dir', value: 'rightup' } // 右上45度移动 +]); +``` + +--- + +### `setFaceDir` + +```typescript +function setFaceDir(dir: Dir2): void; +``` + +| 参数 | 类型 | 说明 | +| ----- | ------ | -------------- | +| `dir` | `Dir2` | 八方向面朝方向 | + +**限制** +仅在非移动状态生效 + +**示例** + +```typescript +// 设置角色面朝左上方 +mover.setFaceDir('leftup'); +``` + +--- + +### `setMoveDir` + +```typescript +function setMoveDir(dir: Dir2): void; +``` + +| 参数 | 类型 | 说明 | +| ----- | ------ | ------------ | +| `dir` | `Dir2` | 基础移动方向 | + +**注意** +影响`forward/backward`指令的实际方向 + +--- + +## 抽象方法 + +### `abstract onMoveStart` + +```typescript +function onMoveStart(controller: IMoveController): Promise; +``` + +**触发时机** +移动队列开始执行时 + +--- + +### `abstract onMoveEnd` + +```typescript +function onMoveEnd(controller: IMoveController): Promise; +``` + +**触发时机** +移动队列完成或被中断时 + +--- + +### `abstract onStepStart` + +```typescript +function onStepStart( + step: MoveStepDir, + controller: IMoveController +): Promise; +``` + +| 参数 | 类型 | 说明 | +| ------------ | ----------------- | ------------ | +| `step` | `MoveStepDir` | 当前移动步骤 | +| `controller` | `IMoveController` | 移动控制器 | + +**返回值** +`Promise`:步骤执行标识码(用于后续传递) + +--- + +### `abstract onStepEnd` + +```typescript +function onStepEnd( + step: MoveStepDir, + code: number, + controller: IMoveController +): Promise; +``` + +| 参数 | 类型 | 说明 | +| ------------ | ----------------- | -------------------------- | +| `code` | `number` | `onStepStart` 返回的标识码 | +| `controller` | `IMoveController` | 移动控制器 | + +--- + +### `abstract onSetMoveSpeed` + +```typescript +function onSetMoveSpeed(speed: number, controller: IMoveController): void; +``` + +| 参数 | 类型 | 说明 | +| ------------ | ----------------- | -------------- | +| `speed` | `number` | 新的移动速度值 | +| `controller` | `IMoveController` | 移动控制器 | + +--- + +## BlockMover + +`BlockMover` 是基于 `ObjectMoverBase` 的内置类,用于实现图块移动。 + +```mermaid +graph LR + BlockMover --> ObjectMoverBase + ObjectMoverBase --> EventEmitter + + click EventEmitter "https://nodejs.org/api/events.html#class-eventemitter" +``` + +### 新增方法 + +```typescript +function bind( + x: number, + y: number, + floorId: FloorIds, + layer: FloorLayer, + dir: Dir = 'down' +): boolean; +``` + +| 参数 | 类型 | 说明 | +| --------- | ------------ | -------------------- | +| `x` | `number` | 图块 X 坐标 | +| `y` | `number` | 图块 Y 坐标 | +| `floorId` | `FloorIds` | 所在楼层 ID | +| `layer` | `FloorLayer` | 图层类型(bg/fg 等) | +| `dir` | `Dir` | 初始方向 | + +**返回值**:绑定成功返回 `true`,若目标正在移动则返回 `false` + +**示例** + +```typescript +if (blockMover.bind(5, 8, 'floor1', 'bg', 'up')) { + blockMover.insertMove({ type: 'dir', value: 'right' }); +} +``` + +--- + +## HeroMover + +`HeroMover` 是基于 `ObjectMoverBase` 的内置类,用于实现勇士移动。 + +```mermaid +graph LR + HeroMover --> ObjectMoverBase + ObjectMoverBase --> EventEmitter + + click EventEmitter "https://nodejs.org/api/events.html#class-eventemitter" +``` + +### 覆盖方法 + +```ts +function startMove( + ignoreTerrain: boolean = false, + noRoute: boolean = false, + inLockControl: boolean = false, + autoSave: boolean = false +): IMoveController | null; +``` + +| 参数 | 说明 | +| --------------- | ---------------------------------------- | +| `ignoreTerrain` | 是否忽略地形,即是否穿墙 | +| `noRoute` | 是否不计入录像 | +| `inLockControl` | 是否是在锁定控制中移动的,例如事件中移动 | +| `autoSave` | 在必要时刻是否自动存档 | + +其余用法与基类相同。 + +--- + +## 使用示例 + +### 勇士移动控制 + +```typescript +import { heroMoverCollection } from '@user/data-state'; + +// 获取勇士移动控制器单例 +const heroMover = heroMoveCollection.mover; + +// 设置面朝方向为右侧 +heroMover.setFaceDir('right'); + +// 添加移动指令:前进三步 +heroMover.insertMove( + { type: 'dir', value: 'forward' }, + { type: 'dir', value: 'forward' }, + { type: 'dir', value: 'forward' } +); + +// 启动移动并获取控制器 +const controller = heroMover.startMove( + false, // 不允许穿墙 + true, // 不计入录像 + false, // 不在录像锁定中触发 + false // 不进行自动存档 +); + +if (controller) { + // 动态追加移动指令 + controller.push({ type: 'dir', value: 'leftup' }); + // 监听移动完成事件 + controller.onEnd.then(() => { + console.log('勇士移动完成'); + }); +} +``` + +### 图块移动控制 + +```typescript +import { BlockMover } from '@user/data-state'; + +// 创建图块移动器实例 +const blockMover = new BlockMover(); + +// 绑定到(5,8)位置的背景图块 +if (blockMover.bind(5, 8, 'floor1', 'bg', 'up')) { + // 添加螺旋移动路径 + blockMover.moveAs([ + { type: 'dir', value: 'right' }, + { type: 'dir', value: 'down' }, + { type: 'dir', value: 'left' }, + { type: 'dir', value: 'up' } + ]); + + // 设置移动速度为200像素/秒 + blockMover.insertMove({ type: 'speed', value: 200 }); + + // 启动移动 + const ctrl = blockMover.startMove(); +} +``` + +--- + +## 移动指令类型 + +```typescript +type MoveStep = + | { type: 'dir'; value: Move2 } // 方向指令 + | { type: 'speed'; value: number }; // 速度指令 +``` + +--- + +## 注意事项 + +1. **方向优先级** + `forward/backward` 基于当前面朝方向计算,修改 faceDir 会影响实际移动方向 + +2. **速度叠加规则** + 多个 speed 指令按队列顺序覆盖,最终生效最后一个速度值 + +3. **移动中断处理** + 调用 controller.stop() 会立即中断移动并触发 moveEnd 事件 diff --git a/docs/api/user-data-state/index.md b/docs/api/user-data-state/index.md new file mode 100644 index 0000000..6360999 --- /dev/null +++ b/docs/api/user-data-state/index.md @@ -0,0 +1,3 @@ +# @user/data-state + +目录: diff --git a/docs/api/user-data-utils/index.md b/docs/api/user-data-utils/index.md new file mode 100644 index 0000000..bfaa99e --- /dev/null +++ b/docs/api/user-data-utils/index.md @@ -0,0 +1,3 @@ +# @user/data-utils + +目录: diff --git a/docs/api/user-entry-client/index.md b/docs/api/user-entry-client/index.md new file mode 100644 index 0000000..6579b3d --- /dev/null +++ b/docs/api/user-entry-client/index.md @@ -0,0 +1,3 @@ +# @user/entry-client + +目录: diff --git a/docs/api/user-entry-data/Mota.md b/docs/api/user-entry-data/Mota.md new file mode 100644 index 0000000..ad02971 --- /dev/null +++ b/docs/api/user-entry-data/Mota.md @@ -0,0 +1,251 @@ +# Mota API 文档 + +本文档由 `DeepSeek R1` 模型生成并微调。 + +--- + +## 核心功能 + +模块化管理系统,提供跨进程模块注册与获取能力,支持数据端与渲染端分离架构。用于解决服务端录像验证与客户端渲染的模块隔离问题。 + +--- + +## 全局访问 + +```typescript +// 浏览器环境 +Mota.require('@motajs/client'); + +// ESM 环境 +import { Mota } from '@motajs/core'; +``` + +--- + +## 核心方法 + +### `require` + +```typescript +function require(key: K): ModuleInterface[K]; +function require(key: string): T; +``` + +**功能** +获取已注册的模块实例 + +| 参数 | 类型 | 说明 | +| ----- | -------- | -------------------------- | +| `key` | `string` | 模块标识符或自定义命名空间 | + +**返回值** +对应模块的导出对象 + +**预定义模块列表**: + +```typescript +interface ModuleInterface { + // ---------- 样板库 + '@motajs/client': typeof Client; + '@motajs/client-base': typeof ClientBase; + '@motajs/common': typeof Common; + '@motajs/legacy-client': typeof LegacyClient; + '@motajs/legacy-common': typeof LegacyCommon; + '@motajs/legacy-system': typeof LegacySystem; + '@motajs/legacy-ui': typeof LegacyUI; + '@motajs/render': typeof Render; + '@motajs/render-core': typeof RenderCore; + '@motajs/render-elements': typeof RenderElements; + '@motajs/render-style': typeof RenderStyle; + '@motajs/render-vue': typeof RenderVue; + '@motajs/system': typeof System; + '@motajs/system-action': typeof SystemAction; + '@motajs/system-ui': typeof SystemUI; + // ---------- 用户扩展 + '@user/client-modules': typeof ClientModules; + '@user/data-base': typeof DataBase; + '@user/data-fallback': typeof DataFallback; + '@user/data-state': typeof DataState; + '@user/data-utils': typeof DataUtils; + '@user/legacy-plugin-client': typeof LegacyPluginClient; + '@user/legacy-plugin-data': typeof LegacyPluginData; + // ---------- 必要的第三方库 + MutateAnimate: typeof MutateAnimate; + Vue: typeof Vue; + Lodash: typeof Lodash; +} +``` + +**示例**: + +```typescript +// 获取动画引擎 +const Animate = Mota.require('MutateAnimate'); + +// 获取Vue实例 +const Vue = Mota.require('Vue'); + +// 获取旧版UI系统 +const LegacyUI = Mota.require('@motajs/legacy-ui'); +``` + +--- + +### `register` + +```typescript +function register( + key: K, + data: ModuleInterface[K] +): void; +function register(key: string, data: unknown): void; +``` + +**功能** +注册模块到全局系统 + +| 参数 | 类型 | 说明 | +| ------ | -------- | ------------ | +| `key` | `string` | 模块标识符 | +| `data` | `any` | 模块导出对象 | + +**注意事项** + +- 重复注册会触发控制台警告 +- 推荐在游戏初始化阶段注册 + +**示例**: + +```typescript +// 注册自定义模块 +class MyCustomModule { + static version = '1.0.0'; +} +Mota.register('@user/custom-module', MyCustomModule); + +// 使用自定义模块 +const custom = Mota.require('@user/custom-module'); +console.log(custom.version); // 输出 1.0.0 +``` + +--- + +## 渲染进程控制 + +### `r` + +```typescript +function r(fn: (this: T) => void, thisArg?: T): void; +``` + +**功能** +包裹只在渲染进程执行的代码 + +| 参数 | 类型 | 说明 | +| --------- | ---------- | -------------------- | +| `fn` | `Function` | 需要渲染端执行的函数 | +| `thisArg` | `any` | 函数执行上下文 | + +**特性** + +- 在录像验证和服务端环境下不会执行 +- 无返回值设计 + +**示例**: + +```typescript +// 播放仅客户端可见的特效 +Mota.r(() => { + const animate = Mota.require('MutateAnimate'); + animate(heroSprite).shake(5, 1000); +}); +``` + +--- + +### `rf` + +```typescript +function rf any, T>( + fn: F, + thisArg?: T +): (...params: Parameters) => ReturnType | undefined; +``` + +**功能** +生成渲染进程安全函数 + +| 参数 | 类型 | 说明 | +| --------- | ---------- | ---------------- | +| `fn` | `Function` | 需要包装的原函数 | +| `thisArg` | `any` | 函数执行上下文 | + +**返回值** +经过安全包裹的函数,在非渲染环境调用返回 `undefined` + +**示例**: + +```typescript +// 创建安全渲染函数 +const safeAlert = Mota.rf((msg: string) => { + alert(`客户端提示: ${msg}`); +}); + +// 调用时自动判断执行环境 +safeAlert('仅在客户端显示'); // 服务端返回 undefined +``` + +--- + +## 架构示意图 + +```mermaid +graph TD + subgraph 数据端 + Validator[录像验证系统] + CoreLogic[核心逻辑] + end + + subgraph 渲染端 + UI[用户界面] + Effects[特效系统] + Render[渲染引擎] + end + + MotaSystem -->|require/register| Validator + MotaSystem -->|r/rf| Render + CoreLogic -->|跨进程通信| Effects +``` + +--- + +## 注意事项 + +1. **模块隔离** + 数据端模块与渲染端模块物理隔离,数据端不可直接引用渲染端,而渲染端是可以直接引用数据端的 + +2. **版本兼容** + 遗留系统模块(legacy-\*)将在未来版本逐步废弃,这些模块也不再提供 API 文档,如果需要自行阅读源码。 + +3. **性能优化** + 高频调用模块建议缓存引用: + + ```typescript + // 推荐 + const Animate = Mota.require('MutateAnimate'); + + // 不推荐 + function update() { + const Animate = Mota.require('MutateAnimate'); // 每次调用都查找 + } + ``` + +4. **错误处理** + 使用 try-catch 包裹高风险模块获取: + ```typescript + try { + const LegacyUI = Mota.require('@motajs/legacy-ui'); + } catch (e) { + fallbackUI(); + } + ``` diff --git a/docs/api/user-entry-data/index.md b/docs/api/user-entry-data/index.md new file mode 100644 index 0000000..e41a4ff --- /dev/null +++ b/docs/api/user-entry-data/index.md @@ -0,0 +1,3 @@ +# @user/entry-data + +目录: diff --git a/docs/api/user-legacy-plugin-client/index.md b/docs/api/user-legacy-plugin-client/index.md new file mode 100644 index 0000000..b1780c5 --- /dev/null +++ b/docs/api/user-legacy-plugin-client/index.md @@ -0,0 +1,3 @@ +# @user/legacy-plugin-client + +目录: diff --git a/docs/api/user-legacy-plugin-data/index.md b/docs/api/user-legacy-plugin-data/index.md new file mode 100644 index 0000000..6da78f1 --- /dev/null +++ b/docs/api/user-legacy-plugin-data/index.md @@ -0,0 +1,3 @@ +# @user/legacy-plugin-data + +目录: diff --git a/docs/guide/audio.md b/docs/guide/audio.md new file mode 100644 index 0000000..e7f5254 --- /dev/null +++ b/docs/guide/audio.md @@ -0,0 +1,393 @@ +# 音频系统 + +2.B 有了与 2.A 完全不同的音频系统,新的音频系统更加自由,功能更加丰富,可以创建多种自定义效果器。本文将讲解如何使用音频系统。 + +:::tip +多数情况下,你应该不需要使用本文所介绍的内容,因为样板已经将音效、背景音乐等处理完善。如果你想实现高级效果,例如混响效果等,才需要阅读本文。 +::: + +## 获取音频播放器 + +音频播放器在 `@user/client-modules` 模块中,直接引入即可: + +```ts +// 在其他模块中使用模块化语法引入 +import { audioPlayer } from '@user/client-modules'; +// 在 client-modules 模块中使用模块化语法引入 +import { audioPlayer } from '../audio'; // 改为你自己的相对路径 + +// 使用 Mota 全局变量引入 +const { audioPlayer } = Mota.require('@user/client-modules'); +``` + +## 音频系统工作流程 + +音频播放流程如下: + +```mermaid +graph LR; + A(音频源) --> B(效果器) --> C(目的地(扬声器、耳机)) +``` + +## 创建音频源 + +:::tip +本小节的内容极大概率用不到,如果不是需要非常底层的音频接口,可以不看本小节。 +::: + +样板内置了几种音频源,它们包括: + +| 类型 | 适用场景 | 创建方法 | +| --------------- | ----------------------- | ----------------------- | +| `BufferSource` | 预加载的完整音频文件 | `createBufferSource()` | +| `ElementSource` | 通过 `` 标签播放 | `createElementSource()` | +| `StreamSource` | 流式音频/长音频 | `createStreamSource()` | + +### `StreamSource` 音频源 + +一般情况下,我们推荐使用 `opus` 格式的音频,这时候需要使用 `StreamSource` 音频源来播放。这个音频源包含了对 IOS 的适配,可以正确播放 `opus` 格式的音频。在使用它之前,我们需要先创建一个 `StreamLoader` 类,来对音频流式加载。假如你在 `project/mybgm/` 文件夹下有一个 `xxx.opus` 音频,你可以这么创建: + +```ts +import { StreamLoader } from '@user/client-modules'; + +const stream = new StreamLoader('project/mybgm/xxx.opus'); +``` + +然后,创建音频源,并将流加载对象泵入音频源: + +```ts +const source = audioPlayer.createStreamSource(); +stream.pipe(source); +stream.start(); // 开始流式加载,如果不需要实时性,也可以不调用,音频播放时会自动开始加载 +``` + +### `ElementSource` 音频源 + +从一个 `audio` 元素创建音频源,假设你想要播放 `project/mybgm/xxx.mp3`,那么你可以这么创建: + +```ts +const source = audioPlayer.createElementSource(); +source.setSource('project/mybgm/xxx.mp3'); +``` + +### `BufferSource` 音频源 + +从音频缓冲创建音频源。音频缓冲是直接存储在内存中的一段原始音频波形数据,不经过任何压缩。假如你想播放 `project/mysound/xxx.wav`,可以这么写: + +```ts +async function loadWav(url: string) { + // 使用浏览器接口 fetch 来请求文件 + const response = await fetch(url); + // 将文件接收为 ArrayBuffer 形式 + const buffer = await response.arraybuffer(); + // 创建音频源 + const source = audioPlayer.createBufferSource(); + // 直接传入 ArrayBuffer,内部会自动解析,当然也可以自己解析,传入 AudioBuffer + await source.setBuffer(source); + // 将音频源返回,供后续使用 + return source; +} +``` + +## 创建音频路由 + +音频路由包含了音频播放的所有流程,想要播放一段音频,必须首先创建音频路由,然后使用 `audioPlayer` 播放这个音频路由。如下例所示: + +```ts +import { AudioRoute, audioPlayer } from '@user/client-modules'; + +const route = audioPlayer.createRoute(source); +``` + +下面,我们需要将音频路由添加至音频播放器: + +```ts +audioPlayer.addRoute('my-route', route); +``` + +之后,我们就可以使用 `audioPlayer` 播放这个音频了: + +```ts +audioPlayer.play('my-route'); +``` + +## 音频效果器 + +新的音频系统中最有用的功能就是音频效果器了。音频效果器允许你对音频进行处理,可以实现调节声道音量、回声效果、延迟效果,以及各种自定义效果等。 + +内置效果器包含这些: + +| 效果器类型 | 功能说明 | 创建方法 | +| --------------------- | ------------------ | --------------------------- | +| `VolumeEffect` | 音量控制 | `createVolumeEffect()` | +| `StereoEffect` | 立体声场调节 | `createStereoEffect()` | +| `EchoEffect` | 回声效果 | `createEchoEffect()` | +| `DelayEffect` | 延迟效果 | `createDelay()` | +| `ChannelVolumeEffect` | 调节某个声道的音量 | `createChannelVolumeEffect` | + +每个效果器都有自己可以调节的属性,具体可以查看对应效果器的 API 文档,比较简单,这里不在讲解。下面主要讲解一下如何使用效果器,我们直接通过例子来看(代码由 `DeepSeek R1` 模型生成并微调): + +```ts +// 创建效果链 +const volume = audioPlayer.createVolumeEffect(); +const echo = audioPlayer.createEchoEffect(); + +// 配置效果参数 +volume.setGain(0.7); // 振幅变为 0.7 倍 +echo.setEchoDelay(0.3); // 回声延迟 0.3 秒 +echo.setFeedbackGain(0.5); // 回声增益为 0.5 + +// 应用效果到音频路由 +const route = audioPlayer.getRoute('my-route')!; +route.addEffect([volume, echo]); + +// 之后播放就有效果了 +route.play(); +``` + +## 空间音效 + +本音频系统还支持空间音效,可以设置听者位置和音频位置,示例如下(代码由 `DeepSeek R1` 模型生成并微调): + +```ts +// 设置听者位置 +audioPlayer.setListenerPosition(0, 1.7, 0); // 1.7米高度 +audioPlayer.setListenerOrientation(0, 0, -1); // 面朝屏幕内 + +// 设置声源位置(使用 StereoEffect 效果器) +const stereo = audioPlayer.createStereoEffect(); +stereo.setPosition(5, 0, -2); // 右方5米,地面下方2米 +``` + +## 淡入淡出效果 + +音频系统提供了淡入淡出接口,可以搭配 `mutate-animate` 库实现淡入淡出效果: + +```ts +import { Transition, linear } from 'mutate-animate'; + +// 创建音量效果器 +const volume = audioPlayer.createVolumeEffect(); + +// 创建渐变类,使用渐变是因为可以避免来回播放暂停时的音量突变 +const trans = new Transition(); +trans.value.volume = 0; + +// 每帧设置音量 +trans.ticker.add(() => { + volume.setVolume(trans.value.volume); +}); + +// 当音频播放时执行淡入 +route.onStart(() => { + // 两秒钟时间线性淡入 + trans.time(2000).mode(linear()).transition('volume', 1); +}); +route.onEnd(() => { + // 三秒钟时间线性淡出 + trans.time(3000).mode(linear()).transition('volume', 0); +}); + +// 添加音量效果器 +route.addEffect(volume); +``` + +## 音效系统 + +为了方便播放音效,音频系统内置提供了音效的播放器,允许你播放空间音效。 + +### 播放音效 + +样板已经自动将所有注册的音效加入到音效系统中,你只需要直接播放即可,不需要手动加载。播放时,可以指定音频的播放位置,听者(玩家)位置可以通过 `audioPlayer.setPosition` 及 `audioPlayer.setOrientation` 设置。示例如下: + +```ts +import { soundPlayer } from '@user/client-modules'; + +// 播放已加载的音效 +const soundId = soundPlayer.play( + 'mysound.opus', + [1, 0, 0], // 音源位置,在听者前方 1m 处 + [0, 1, 0] // 音源朝向,朝向天花板 +); + +// 停止指定音效 +soundPlayer.stop(soundId); +// 停止所有音效 +soundPlayer.stopAllSounds(); +``` + +### 设置是否启用音效 + +你可以自行设置是否启用音效系统: + +```ts +soundPlayer.setEnabled(false); // 关闭音效系统 +soundPlayer.setEnabled(true); // 启用音效系统 +``` + +## 音乐系统 + +音乐系统的使用与音频系统类似,包含播放、暂停、继续等功能。示例如下: + +```ts +import { bgmController } from '@user/client-modules'; + +bgmController.play('bgm1.opus'); // 切换到目标音乐 +bgmController.pause(); // 暂停当前音乐,会有渐变效果 +bgmController.resume(); // 继续当前音乐,会有渐变效果 + +bgmController.blockChange(); // 禁用音乐切换,之后调用 play, pause, resume 将没有效果 +bgmController.unblockChange(); // 启用音乐切换 +``` + +## 自定义效果器 + +本小节内容由 `DeepSeek R1` 模型生成并微调。 + +效果器是新的音频系统最强大的功能,而且此系统也允许你自定义一些效果器,实现自定义效果。效果器的工作流程如下: + +```mermaid +graph LR + Input[输入源] --> EffectInput[效果器输入] + EffectInput --> Processing[处理节点] + Processing --> EffectOutput[效果器输出] + EffectOutput --> NextEffect[下一效果器] +``` + +:::info +这一节难度较大,如果你不需要复杂的音效效果,不需要看这一节。 +::: + +### 创建效果器类 + +所有效果器都需要继承 `AudioEffect` 抽象类,需要实现这些内容: + +```ts +abstract class AudioEffect implements IAudioInput, IAudioOutput { + abstract output: AudioNode; // 输出节点 + abstract input: AudioNode; // 输入节点 + abstract start(): void; // 效果激活时调用 + abstract end(): void; // 效果结束时调用 +} +``` + +### 实现效果器 + +下面以一个双线性低通滤波器为例,展示如何创建一个自定义滤波器。首先,我们需要继承 `AudioEffect` 抽象类: + +```ts +class CustomEffect extends AudioEffect { + // 实现抽象成员 + output: AudioNode; + input: AudioNode; +} +``` + +接下来,我们需要构建音频节点,创建一个 `BiquadFilter`: + +```ts +class CustomEffect extends AudioEffect { + constructor(ac: AudioContext) { + super(ac); + + // 创建处理节点链 + const filter = ac.createBiquadFilter(); // 滤波器节点 + filter.type = 'lowpass'; // 低通滤波器 + // 输入节点和输出节点都是滤波器节点 + this.input = filter; + this.output = filter; + } +} +``` + +然后,我们可以提供接口来让外部能够调整这个效果器的参数: + +```ts +class CustomEffect extends AudioEffect { + private Q: number = 1; + private frequency: number = 1000; + + /** 设置截止频率 */ + setCutoff(freq: number) { + this.frequency = Math.min(20000, Math.max(20, freq)); + this.output.frequency.value = this.frequency; + } + + /** 设置共振系数 */ + setResonance(q: number) { + this.Q = Math.min(10, Math.max(0.1, q)); + this.output.Q.value = this.Q; + } +} +``` + +最后,别忘了实现 `start` 方法和 `end` 方法,虽然不需要有任何内容: + +```ts +class CustomEffect extends AudioEffect { + start() {} + end() {} +} +``` + +### 使用效果器 + +就如内置的效果器一样,创建效果器实例并添加入路由图即可: + +```ts +const myEffect = new CustomEffect(audioPlayer.ac); +myRoute.addEffect(myEffect); +``` + +### 高级技巧 + +动画修改属性: + +```ts +// 创建参数渐变 +rampFrequency(target: number, duration: number) { + const current = this.output.frequency.value; + this.output.frequency.setValueAtTime(current, this.ac.currentTime); + this.output.frequency.linearRampToValueAtTime( + target, + this.ac.currentTime + duration + ); +} +``` + +在一个效果器内添加多个音频节点: + +```ts +class ReverbEffect extends AudioEffect { + private convolver: ConvolverNode; + private wetGain: GainNode; + + constructor(ac: AudioContext) { + super(ac); + this.input = ac.createGain(); // 输入增益节点 + this.wetGain = ac.createGain(); // 卷积增益节点 + this.convolver = ac.createConvolver(); // 卷积节点 + + // 构建混合电路 + const dryGain = ac.createGain(); // 原始音频增益节点 + this.input.connect(dryGain); + this.input.connect(this.convolver); + this.convolver.connect(this.wetGain); + + // 合并输出 + const merger = ac.createChannelMerger(); + dryGain.connect(merger, 0, 0); + this.wetGain.connect(merger, 0, 1); + this.output = merger; + } +} +``` + +以上效果器的流程图如下: + +```mermaid +graph LR; + A(input 增益节点) --> B(dryGain 增益节点); + A --> C(convolver 卷积节点) --> D(wetGain 增益节点) + B & D --> E(output 声道合并节点) +``` diff --git a/docs/guide/coding.md b/docs/guide/coding.md new file mode 100644 index 0000000..2f2a352 --- /dev/null +++ b/docs/guide/coding.md @@ -0,0 +1,50 @@ +# 代码编写指南 + +本节将介绍如何在 2.B 样板中正确编写代码 + +## 在哪编写代码? + +我们推荐在 `packages-user` 中编写代码,在这里你可以享受到完整的类型支持与代码补全。展开 `packages-user` 文件夹,可以看到里面分为了多个文件夹。我们应该主要在 `client-modules` 和 `data-state` 中编写代码,分别是渲染层和数据层的主要代码位置。其余内容一般是系统底层相关的代码,如果看不懂的话不建议修改。 + +```mermaid +graph LR + root(package-user) + root --> A(client-modules) --> A1(渲染层主要代码) + root --> B(data-base) --> B1(数据层底层代码) + root --> C(data-fallback) --> C1(数据层向后兼容代码) + root --> D(data-state) --> D1(数据层游戏状态代码) + root --> E(data-utils) --> E1(数据层工具代码) + root --> F(entry-client) --> F1(渲染层入口代码) + root --> G(entry-data) --> G1(数据层入口代码) + root --> H(legacy-plugin-client) --> H1(遗留渲染层代码) + root --> I(legacy-plugin-data) --> I1(遗留数据层代码) +``` + +## 如何使用模块化引入? + +在引入时,需要**严格**遵循如下原则: + +1. 同一模块间使用相对路径引入 `./xxx` +2. 不同模块间使用绝对路径引入 `@user/xxxx` + +例如,如果你在 `@user/client-modules` 中引入自身的代码,就需要使用 `import { xxx } from './xxx`,如果你需要在 `@user/client-modules` 引入 `@user/data-base` 中的代码,就需要使用 `import { xxx } from '@user/data-base'` 引入。 + +:::warning +注意,在之后的所有文档示例中,都会使用 `import xxx from '@xxx/xxx'` 的绝对路径形式作为示例,而不会使用相对路径,自己编写代码时请注意要引入的内容是否在当前模块(当前包)中,如果是,请使用相对路径,否则请使用绝对路径。 +::: + +## 使用 TypeScript + +所有代码使用 `TypeScript` 编写,后缀名为 `.ts` 或 `.tsx`,其中前者表示一般代码,后者表示 UI 代码(即包含 XML 的代码) + +编写代码时,需要保证类型正确。如果搞不明白类型系统,类型就写 `any`(但是极其不推荐!这会使得自动补全也消失!) + +`TypeScript` 类型系统教程可以查看[我编写的教程](https://h5mota.com/bbs/thread/?tid=1018&p=3#p41) + +## 渲染层与数据层分离 + +渲染层可以直接引用数据层代码,但是数据层**不能**直接引用渲染层代码,具体请查看[系统说明](./system.md#渲染端与数据端通信) + +## 避免循环引用依赖 + +需要避免两个包之间循环引入。如果出现了循环引入,请考虑将它们挪到一个包里面,或者将循环的部分单独拿出来作为一个包。 diff --git a/docs/guide/diff.md b/docs/guide/diff.md new file mode 100644 index 0000000..0319ff6 --- /dev/null +++ b/docs/guide/diff.md @@ -0,0 +1,34 @@ +--- +lang: zh-CN +--- + +# 差异说明 + +本文档暂时只会对新样板新增内容进行说明,其余请查看[旧样板文档](https://h5mota.com/games/template/_docs/#/)。 + +本指南建立在你已经大致了解 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 等作为了开发语言。 + +本文将描述 2.B 样板与 2.10.3 及 2.A 样板的差异。 + +## 注意事项 + +对于新样板,由于拥有了近乎完整的类型标注,因此更推荐使用 `VS Code` 进行代码编写,这样你可以获取到完整的类型标注,而由于类型标注的复杂性,样板编辑器完全无法部署,因此样板编辑器不会有任何新版的类型标注。在之后的更新中,样板 API 会进行大幅度的改动,因此每次更新都可能会弃用一部分 API,同时这些 API 会在若干个版本后被彻底删除。因此如果你的代码中使用到了弃用的 API,请尽快更换写法以保证可以向后接档。 + +## 主要差异 + +- 开发语言换为 TypeScript,可以享受到完整的类型支持 +- 使用全新的 UI 编写方式,速度快,效率高 +- 模块化,可以使用 ES6 模块化语法 +- 移除插件系统,可以自定义代码目录结构,更加自由 +- 优化渲染端(client 端)与数据端(data 端)的通讯,渲染端现在可以直接引用数据端,不过数据端还不能直接引用渲染端 + +## 差异内容 + +相比于 2.10.3 及 2.A,有如下改动: + +- [系统说明](./system) +- [UI 编写](./ui) +- [UI 系统](./ui-system) +- [音频系统](./audio) diff --git a/docs/guide/img/image.png b/docs/guide/img/image.png new file mode 100644 index 0000000..bcc2908 Binary files /dev/null and b/docs/guide/img/image.png differ diff --git a/docs/guide/img/mermaid-diagram-2025-03-12-210212.svg b/docs/guide/img/mermaid-diagram-2025-03-12-210212.svg new file mode 100644 index 0000000..2105c7e --- /dev/null +++ b/docs/guide/img/mermaid-diagram-2025-03-12-210212.svg @@ -0,0 +1,3 @@ + + +是否加载 index.html加载 2.x 样板的第三方库是否在游戏中?加载渲染端入口加载数据端入口初始化数据端,写入 Mota 全局变量dataRegistered初始化渲染端clientRegisteredregistered执行数据端各个模块的初始化函数执行渲染端各个模块的初始化函数加载数据端入口初始化数据端,写入 Mota 全局变量dataRegistered 与 registered执行数据端各个模块的初始化函数执行 main.js 初始化加载全塔属性加载 core.js 及其他 libs 中的脚本coreInit开始资源加载自动元件加载完毕后触发 autotileLoaded资源加载完毕后触发 loaded进入标题界面 \ No newline at end of file diff --git a/docs/guide/system.md b/docs/guide/system.md new file mode 100644 index 0000000..58aa70e --- /dev/null +++ b/docs/guide/system.md @@ -0,0 +1,192 @@ +--- +lang: zh-CN +--- + +# 系统说明 + +本文将介绍 2.B 的系统都做了哪些更改 + +## 模块化 + +2.B 样板现在已经迁移至了 monorepo,将代码模块化,共分为 20 余个模块,每个模块的具体内容可以参考 API 文档,模块列表如下: + +- [@motajs/client](../api/motajs-client) 渲染层代码 +- [@motajs/client-base](../api/motajs-client-base) 渲染层底层代码 +- [@motajs/common](../api/motajs-common) 渲染层和数据层通用代码 +- [@motajs/legacy-client](../api/motajs-legacy-client) 遗留渲染层代码 +- [@motajs/legacy-common](../api/motajs-legacy-common) 遗留通用代码 +- [@motajs/legacy-data](../api/motajs-legacy-data) 遗留数据层代码 +- [@motajs/legacy-system](../api/motajs-legacy-system) 遗留渲染层系统代码 +- [@motajs/legacy-ui](../api/motajs-legacy-ui) 遗留 UI 相关代码 +- [@motajs/render](../api/motajs-render) 渲染系统代码 +- [@motajs/render-core](../api/motajs-render-core) 渲染系统核心代码 +- [@motajs/render-elements](../api/motajs-render-elements) 渲染系统内置元素代码 +- [@motajs/render-style](../api/motajs-render-style) 渲染系统样式代码 +- [@motajs/render-vue](../api/motajs-render-vue) 渲染系统 vue 支持代码 +- [@motajs/system](../api/motajs-system) 渲染层系统代码 +- [@motajs/system-action](../api/motajs-system-action) 渲染层交互系统代码 +- [@motajs/system-ui](../api/motajs-system-ui) 渲染层 UI 系统代码 +- [@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) 用户数据层向后兼容代码 +- [@user/data-state](../api/user-data-state) 用户数据层状态代码 +- [@user/data-utils](../api/user-data-utils) 用户数据层工具代码 +- [@user/entry-client](../api/user-entry-client) 用户渲染层入口 +- [@user/entry-data](../api/user-entry-data) 用户数据层入口 +- [@user/legacy-plugin-client](../api/user-legacy-plugin-client) 用户遗留渲染层代码 +- [@user/legacy-plugin-data](../api/user-legacy-plugin-data) 用户遗留数据层代码 + +## Mota 全局变量 + +与 2.A 不同,2.B 对 `Mota` 全局变量做了简化,不再拥有 `Mota.Plugin` `Mota.Package` `Mota.requireAll` 属性与方法,它们全部整合至了 `Mota.require` 方法中,同时该方法的用法与 2.A 也不同,在 2.A 中,我们往往使用 `Mota.require('var', 'xxx')` 的方式调用,繁琐且不直观。在 2.B 中,我们可以直接填入模块名称,就可以获取到其内容了,例如: + +```ts +const { hook, loading } = Mota.require('@user/data-base'); // 获取 hook 与 loading +const { Font } = Mota.require('@motajs/render'); // 获取 Font 字体类 +``` + +我们只需要填写一个参数,而不需要填写两个参数了,更加直观,而且与 ES6 模块语法类似,便于转换。 + +多数情况下,我们是不需要使用 `Mota` 全局变量的。不过,还是有一些特殊情况需要使用该全局变量才可以,这些情况包括: + +- 在数据端调用渲染端接口,数据端需要跑录像验证,因此不能直接引入渲染端接口,需要通过此全局变量才可以。 +- 在 `libs` `functions.js` 中调用接口,这两个地方暂时还没有模块化,因此无法直接引入,需要通过此全局变量调用。 + +## 渲染端与数据端通信 + +一般情况下,渲染端**可以**直接引入数据端的内容,例如你可以在 `@user/client-modules` 里面直接引入 `@user/data-state` 的接口,这是没有问题的。不过,由于数据端需要在服务器上跑录像验证,因此**不能**直接引入渲染端的内容,否则会导致验证报错。如果需要在数据端引用渲染端接口,我们需要这么做: + +```ts +// @user/data-state 中的某文件 +const num = 100; +Mota.r(() => { + // 使用 r 方法包裹,这样这个函数就会在渲染端运行,可以有返回值,但是在录像验证中只会是 undefined + const { Font } = Mota.require('@motajs/render'); + const font = new Font('Verdana', 18); + // 函数内也可以调用外部变量,例如这里就调用了外部的 num 变量,但是极度不推荐在渲染端修改数据端的内容 + // 否则很可能导致录像不能运行,这里这个例子就会导致录像运行出错,因为录像验证时并不会执行这段代码, + // 勇士的血量也就不会变大,于是就出错了。 + core.status.hero.hp += num; +}); +``` + +除此之外,我们还可以使用钩子来进行数据通信。示例如下: + +```ts +// 渲染端和数据端都可以使用这个方式引入 +import { hook } from '@user/data-base'; +// 也可以通过 Mota.require 方法引入 +const { hook } = Mota.require('@user/data-base'); + +// 监听战后函数,每次与怪物战斗后,都会执行这个函数 +// 每个钩子的参数定义可以参考 package-user/data-base/src/game.ts GameEvent 接口 +hook.on('afterBattle', enemy => { + console.log('与怪物战斗:', enemy.id); +}); +``` + +## 加载流程 + +与 2.A 相比,加载流程也不太一样,下面是 2.B 的加载流程: + +1. 加载 `index.html` +2. 加载 2.x 样板的第三方库 + +3. 如果是游戏中,加载 `src/main.ts` + + 1. 加载渲染端入口 + 2. 加载数据端入口 + 3. 并行初始化数据端与渲染端,在数据端写入 `Mota` 全局变量 + 4. 数据端初始化完毕后执行 `loading.emit('dataRegistered')` 钩子,渲染端初始化完毕后执行 `loading.emit('clientRegistered')` 钩子 + 5. 二者都初始化完毕后执行 `loading.emit('registered')` 钩子 + 6. 执行数据端各个模块的初始化函数 + 7. 执行渲染端各个模块的初始化函数 + +4. 如果是录像验证中: + + 1. 加载数据端入口 + 2. 初始化数据端,写入 `Mota` 全局变量 + 3. 初始化完毕后执行 `loading.emit('dataRegistered')` 与 `loading.emit('registered')` 钩子 + 4. 执行数据端各个模块的初始化函数 + +5. 执行 `main.js` 初始化 +6. 加载全塔属性 +7. 加载 `core.js` 及其他 `libs` 中的脚本 +8. 加载完毕后执行 `loading.emit('coreInit')` 钩子 +9. 开始资源加载 +10. 自动元件加载完毕后执行 `loading.emit('autotileLoaded')` 钩子 +11. 资源加载完毕后执行 `loading.emit('loaded')` 钩子 +12. 进入标题界面 + +使用流程图表示如下: + + + +## 函数重写 + +在 2.B 模式下,如果想改 `libs` 的内容,如果直接在里面改会很麻烦,而且两端通讯也不方便,因此我们建议在 `package-user` 中对函数重写,这样的话就可以使用模块化语法,更加方便。同时,2.B 也提供了函数重写接口,他在 `@motajs/legacy-common` 模块中,我们可以这么使用它: + +```ts +// 新建一个 ts 文件,例如叫做 override.ts,放在 client-modules 文件夹下 +import { Patch, PatchClass } from '@motajs/legacy-common'; + +// 新建函数,这个操作是必要的,我们不能直接在顶层使用这个接口 +export function patchMyFunctions() { + // 创建 Patch 实例,参数表示这个 Patch 示例要重写哪个文件中的函数 + // 如果需要复写两个文件,那么就需要创建两个实例 + const patch = new Patch(PatchClass.Control); + + // 使用 add 函数来重写,第一个参数会有自动补全 + // 如果要重写的函数以下划线开头,可能会有报错 + // 这时候需要去 types/declaration 中对应的文件中添加声明 + patch.add('getFlag', (name, defaultValue) => { + // 重写 getFlag,如果变量是数字,那么 +100 后返回 + const value = core.status?.hero?.flags[name] ?? defaultValue; + return typeof value === 'number' ? value + 100 : value; + }); +} +``` + +然后,我们找到 `client-modules` 文件夹下的 `index.ts` 文件,然后在 `create` 函数中引入并调用 `patchMyFunctions`,这样我们的函数重写就完成了。**注意**,如果两个重写冲突,会在控制台弹出警告,并使用最后一次重写的内容。 + +::: warning +**注意**,在渲染端重写的函数在录像验证中将无效,因为录像验证不会执行任何渲染端内容! +::: + +## 目录结构 + +我们建议每个文件夹中都有一个 `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'; +``` + +:::warning +注意,在之后的所有文档示例中,都会使用 `import xxx from '@xxx/xxx'` 的绝对路径形式作为示例,而不会使用相对路径,自己编写代码时请注意要引入的内容是否在当前模块(当前包)中,如果是,请使用相对路径,否则请使用绝对路径。 +::: + +然后是从当前模块导出内容: + +```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) diff --git a/docs/guide/ui-elements.md b/docs/guide/ui-elements.md new file mode 100644 index 0000000..9e35192 --- /dev/null +++ b/docs/guide/ui-elements.md @@ -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 + +``` + +除了 `loc` 属性之外,还可以通过设置 `anc` 属性来修改锚点位置,示例如下: + +```tsx +// 设置锚点,效果为靠右对齐,上下居中对齐 + +``` + +你还可以手动指定 `x` `y` `width` `height` `anchorX` `anchorY` 属性,但是这种方式比较啰嗦,并不建议使用: + +```tsx + +``` + +最后说明一下元素的 `type` 属性,此属性描述了元素的定位模式,默认为 `static` 常规定位,此定位模式下元素位置会按照上述内容更改,而在 `absolute` 模式下,不论怎么修改定位属性,它都会保持在左上角的位置,可能会在一些特殊场景下使用(极度不建议使用此属性,很可能在 2.B.1 版本就会将其删除)。 + +### 纵深属性 + +可以通过 `zIndex` 属性来调整一个元素的纵深。纵深描述了元素之间的重叠关系,纵深高的会处在纵深低的元素上方,同时也会阻碍交互事件向纵深低的元素传播。必要的时候,需要通过设置纵深属性来调整层级关系。未设置时,后面的元素会处在前面的元素之上。 + +```tsx +// 这个元素会处在上层 + +// 这个元素会处在下层 + +``` + +### 效果属性 + +效果属性包含 `filter` `composite` 及 `alpha` 三个属性。 + +`filter` 表示此元素的滤镜,参考 [CanvasRenderingContext2D.filter](https://developer.mozilla.org/zh-CN/docs/Web/CSS/filter),可以填写内置函数或 svg 滤镜。默认不包含任何滤镜。示例如下: + +```tsx +// 亮度变为 150%,对比度变为 120% + +``` + +`composite` 属性描述了当前元素与在此之前渲染的元素之间的混合模式,参考 [CanvasRenderingContext2D.globalCompositeOperation](https://developer.mozilla.org/zh-CN/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation),可以填写 26 个值。默认使用简单 `alpha` 混合,即 `source-over`。例如: + +```tsx +// 使用加算方式叠加,两个颜色的 RGB 值分别相加得到最终结果 + +``` + +`alpha` 属性描述了此元素的不透明度,1 表示完全不透明,0 表示完全透明。在叠加时,所有颜色都会乘以此不透明度后叠加。默认值是完全不透明,即 1。但需要注意的是,虽然 1 表示完全不透明,但是如果画布内容本身包含透明内容(例如一个半透明矩形),即使是 1 也会表现为透明,因为叠加时会乘以 1,不透明度不变。示例如下: + +```tsx +// 一个半透明元素 + +``` + +### 缓存属性 + +可以通过 `cache` 和 `nocache` 属性来指定这个元素的缓存行为,其中 `nocache` 表示禁用此元素的缓存机制,优先级最高,设置后必然不使用缓存。`cache` 表示启用此元素的缓存行为,常用于一些默认不启用缓存的元素,优先级低于 `cache`。这两个元素都不能动态设置,也就是说不能使用响应式来修改其值。示例如下: + +```tsx +// 内部渲染内容比较简单,不需要启用缓存 + + + +// 路径较为复杂,因此启用 g-path 的缓存行为 + +``` + +### 元素溢出行为 + +溢出行为是指,当其子元素超出父元素的大小时,执行的行为。例如,假如父元素大小为 `200 * 200`,里面有一个子元素,大小为 `100 * 100`,位于 `(150, 50)` 的位置,这时候子元素的一部分就会超出父元素的范围。 + +在本渲染系统中,所有元素的默认溢出行为是裁剪,即不会显示任何溢出内容,注意调整容器的宽高。在 `nocache` 模式下,由于不受到缓存的约束,溢出内容依然会显示,不过不建议利用此特性来编写 UI,因为这种行为可能会在后续的更新中修改。 + +### 隐藏元素 + +可以使用 `hidden` 属性来隐藏元素: + +```tsx +const hidden = ref(false); +// 一般使用一个响应式变量来控制隐藏行为,因为设置成常量没有任何意义 +; +``` + +### 交互属性 + +交互属性包括 `cursor` 和 `noevent`。前者描述了鼠标覆盖在当前元素上时的指针样式,参考 [CSS: cursor](https://developer.mozilla.org/zh-CN/docs/Web/CSS/cursor)。示例如下: + +```tsx +// 鼠标放置在该元素上时使用小手样式 + +``` + +`noevent` 表明当前元素将不会触发任何事件,事件将会下穿至纵深更低的元素。示例如下: + +```tsx +// 设置为 noevent 模式 + +// 这样的话这个 onClick 就可以正常触发了 + +``` + +### 高清与抗锯齿 + +包含 `hd` `anti` `noanti` 三个属性,`hd` 表示是否启用高清,大部分元素是默认启用的,除了几个像素风为主的元素(地图渲染等);`anti` 表示手动启用画布的抗锯齿行为,一般用于默认不启用抗锯齿的元素;`noanti` 表示手动关闭元素的抗锯齿行为,优先级高于 `anti`,一般用于像素风图片展示、图标显示等,同时也有助于提高渲染性能。 + +```tsx +// 关闭高清 + +// 关闭抗锯齿 + +// 启用地图渲染的抗锯齿 + +``` + +### 元素变换属性 + +可以通过调整 `transform` 属性来修改元素的线性变换,包括平移、旋转、缩放。如果是简易的变换,可以使用 `rotate` `scale` 属性来修改旋转、缩放,使用 `loc` 来修改位置: + +```tsx +// 旋转 90 度,横向放缩为 1.5 倍,纵向不放缩 + +``` + +我们没有设置锚点属性,那么需要注意旋转后,`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 + +``` + +#### 常见问题排查 + +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); + }); +}; + +; +``` + +## `text` 标签 + +`text` 标签用于显示文字,它会自动计算文字的宽高并设置为元素宽高,因此不要手动指定宽高,否则可能会引起位置错误。它新增了这些属性: + +```ts +interface TextProps extends BaseProps { + /** 要渲染的文字 */ + text?: string; + /** 文字的填充样式 */ + fillStyle?: CanvasStyle; + /** 文字的描边样式 */ + strokeStyle?: CanvasStyle; + /** 文字的字体 */ + font?: Font; + /** 文字的描边粗细 */ + strokeWidth?: number; +} +``` + +典型案例如下: + +```tsx +import { Font } from '@motajs/render'; + +; +``` + +## `image` 标签 + +`image` 标签允许你显示一张图片,包含一个 `image` 属性,传入图片对象(注意不是注册图片名称)。用例如下: + +```tsx +// 获取注册的图片 +const img = core.material.images.images['myImage.png']; +// 显示图片 +; +``` + +## `icon` 标签 + +`icon` 标签用于显示一个图标,可以包含动画帧。它有如下参数: + +```ts +export interface IconProps extends BaseProps { + /** 图标 id 或数字 */ + icon: AllNumbers | AllIds; + /** 显示图标的第几帧 */ + frame?: number; + /** 是否开启动画,开启后 frame 参数无效 */ + animate?: boolean; +} +``` + +使用案例如下: + +```tsx +// 显示绿史莱姆,开启动画 + +``` + +## `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 像素 + +``` + +## 图形标签 + +本小节讲解图形相关的标签,以下内容由 `DeepSeek R1` 模型生成并稍作修改。 + +### 通用属性说明 + +所有图形元素均支持以下核心属性: + +| 属性分类 | 关键参数 | 说明 | +| -------------- | ------------------------- | -------------------------------------------------------- | +| **填充与描边** | `fill` `stroke` | 控制是否填充/描边(不同元素默认值不同) | +| **样式控制** | `fillStyle` `strokeStyle` | 填充和描边样式,支持颜色/渐变等(如 `'#f00'`) | +| **线型设置** | `lineWidth` `lineDash` | 线宽、虚线模式(如 `[5, 3]` 表示 5 像素实线+3 像素间隙) | +| **高级控制** | `fillRule` `actionStroke` | 填充规则(非零/奇偶)、是否仅在描边区域响应交互 | + +### 矩形 `` + +矩形的定位直接使用 `loc` 即可,示例如下: + +```tsx +// 基础矩形,矩形默认仅填充模式,因此如果需要描边的话需要手动指定 fill 和 stroke 参数 +// 注意如果仅指定 stroke 参数的话,会变为仅描边形式 + +``` + +### 圆形和扇形 `` + +参数如下: + +```ts +interface CirclesProps { + radius: number; // 半径 + start?: number; // 起始弧度(默认0) + end?: number; // 结束弧度(默认2π) + /** + * 圆属性参数,可以填 `[圆心 x 坐标,圆心 y 坐标,半径,起始角度,终止角度]`,是 x, y, radius, start, end 的简写, + * 其中半径可选,后两项要么都填,要么都不填 + */ + circle?: CircleParams; +} +``` + +示例如下: + +```tsx +// 完整圆形 + +// 扇形(60度到180度) + +``` + +### 直线 `` + +核心参数: + +```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 +// 普通直线 + + +// 带箭头的参考线 + +``` + +### 三次贝塞尔曲线 `` + +核心参数: + +```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 +// 三次贝塞尔曲线 +; + +// 动态路径 +const path = computed(() => [ + startX.value, + startY.value, + control1X.value, + control1Y.value, + control2X.value, + control2Y.value, + endX.value, + endY.value +]); +; +``` + +### 二次贝塞尔曲线 `` + +核心参数: + +```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 +// 二次贝塞尔曲线 +; + +// 动态路径 +const path = computed(() => [ + startX.value, + startY.value, + controlX.value, + controlY.value, + endX.value, + endY.value +]); +; +``` + +### 圆角矩形 `` + +圆角矩形的核心参数与 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 的圆角矩形 + +// 每个角都是横向半径为 10,纵向半径为 5 的椭圆 + +// 左上和右下是半径为 10 的圆角,左下和右上是半径为 25 的圆角 + +``` + +### 自定义路径 `` + +核心参数: + +```ts +interface PathProps { + path?: Path2D; // 自定义路径对象 +} +``` + +示例: + +```tsx +// 创建五角星 +const starPath = new Path2D(); +// ...路径绘制逻辑 +; +``` + +### 最佳实践建议 + +1. 交互增强: + +```tsx + +``` + +2. 样式复用: + +```tsx +// 创建样式对象 +const themeStyle = { + fillStyle: '#2c3e50', + strokeStyle: '#ecf0f1', + lineWidth: 2 +}; + + + +``` diff --git a/docs/guide/ui-faq.md b/docs/guide/ui-faq.md new file mode 100644 index 0000000..53a16fc --- /dev/null +++ b/docs/guide/ui-faq.md @@ -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 元素 +watch(data, () => mySprite.value?.update()); + +// 将 mySprite 传入 ref 参数,这样当挂载完毕后就会将 mySprite.value 设置为该元素 +; +``` + +除此之外还可能你的数据不是响应式数据,确保你的数据经过了 `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。 diff --git a/docs/guide/ui-perf.md b/docs/guide/ui-perf.md new file mode 100644 index 0000000..1c91895 --- /dev/null +++ b/docs/guide/ui-perf.md @@ -0,0 +1,142 @@ +--- +lang: zh-CN +--- + +# UI 优化指南 + +多数情况下,我们编写的简单 UI 并不需要特别的性能优化,渲染系统的懒更新及缓存机制已经可以有很优秀的性能表现,不过我们还是可能会遇到一些需要特殊优化的场景,本节将会讲述如何优化 UI 的性能表现,优化建议包括:避免元素平铺;使用 `Scroll` 或 `Page` 组件优化平铺性能;避免元素自我更新;使用 `cache` 和 `nocache` 标识;特殊场景禁用抗锯齿和高清;在合适场景下隐藏一些元素等。 + +## 避免元素平铺 + +在不使用 `Scroll` 组件时,我们需要尽量避免元素平铺,因为这会导致更新时渲染次数上升,从而引起性能下降。我们建议善用树形结构的缓存特性,将可以作为一个整体的元素使用一个容器 `container` 包裹起来,减少更新时的渲染次数,尤其是对于那些不常更新的元素来说,更应该使用容器包裹。不过我们也不建议嵌套过深,这可能导致浪费在递归渲染上的时间过长,渲染效率变低。 + +画布渲染树的深度遍历特性使得: + +- 每个独立容器的更新会触发子树的重新渲染 +- 容器层级过深会增加递归调用栈开销 +- 合理分组可将高频/低频更新元素隔离 + +下面是代码示例: + +```tsx +// ❌ 差的写法,全部平铺在一个容器里 + + + {/* 中间省略 998 个元素 */} + + + +// ✅ 好的写法 + + {/* 把不常更新的单独放到一个容器里面 */} + + + {/* 中间省略 988 个元素 */} + + + {/* 把常更新的单独放到一个容器里面 */} + + + {/* 中间省略 8 个元素 */} + + + +``` + +## 使用 `Scroll` 或 `Page` 组件优化平铺性能 + +在一些特殊情况下,我们不得不使用平铺布局,例如上一节提到的怪物手册,或是展示一个列表等,这时候必须平铺元素。这时候我们可以使用 `Scroll` 组件或 `Page` 组件来优化性能表现。`Scroll` 组件中,只有在画面内的元素会被渲染,而画面外的不会被渲染,这会大大提高渲染效率;`Page` 组件允许你把列表拆分成多个部分,然后把内容放在不同页中,从而提高渲染性能。极端情况下,`Page` 组件的渲染效率要明显高于 `Scroll` 组件,但是滚动条对于交互更友好,我们推荐在简单场景下使用 `Scroll` 组件,而对于复杂场景,换为 `Page` 组件。两个组件的使用方式可以参考 [API 文档](../api/motajs-render-elements/)。 + +我们建议: + +1. **优先使用 Scroll**: + - 元素数量 < 500 + - 需要流畅滚动交互 + - 元素高度不固定 +2. **切换至 Page**: + - 元素数量 > 1000 + - 需要支持快速跳转 + - 存在复杂子组件(如嵌套动画) + +下面是代码示例: + +```tsx +// ❌ 差的写法,全部平铺在一个容器里 + + + {/* 中间省略 998 个元素 */} + + + +// ✅ 好的写法,使用 Scroll 组件优化 + + + {/* 中间省略 998 个元素 */} + + + +// ✅ 好的写法,使用 Page 组件优化 + + {(page: number) => { + return list.slice(page * 10, (page + 1) * 10).map(v => ) + }} + +``` + +## 避免元素自我更新 + +元素自我更新是指,在元素的渲染函数内,触发了元素的冒泡更新,这会导致更新无限循环,而且难以察觉。为了解决难以察觉的问题,我们使用了一种方式来专门探测这种情况。常见的触发元素自我更新的场景就是使用 `sprite` 元素,例如: + +```tsx +const element = ref(); +const render = () => { + element.value?.update(); +}; + +; +``` + +在上面这段渲染代码中,`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 默认启用缓存,可能会拉低渲染效率 +; + +// ✅ 好的写法,使用 nocache 标识禁用 sprite 的缓存机制 +; +``` + +## 特殊场景禁用抗锯齿和高清 + +默认情况下,大部分元素都是默认启用高清即抗锯齿的(`layer` 和 `layer-group` `icon` 不启用),这可能会导致一些不必要的计算出现,从而拉低渲染性能。对于一些需要保持像素风的内容,我们建议关闭抗锯齿和高清画布。代码示例如下: + +```tsx +// ❌ 差的写法,像素风图片使用默认设置,启用了抗锯齿和高清 + +// ✅ 好的写法,关闭了默认的抗锯齿和高清 + +``` + +## 在合适场景下隐藏一些元素 + +如果一个元素在某些场景下需要隐藏,另一些场景下需要显示,我们建议使用 `hidden` 属性来设置,而不是通过把它移动到画面外、调成透明颜色、使用 `if` 或三元表达式判断等方式。示例代码如下: + +```tsx +// ❌ 差的写法,使用条件表达式切换元素显示与否 +{ + !hidden.value && ; +} +// ✅ 好的写法,使用 hidden 属性 +; +``` + +## 后续计划 + +我们后续计划推出渲染树调试工具,届时可以更加细致方便地查看渲染树的渲染情况以及性能问题。 diff --git a/docs/guide/ui-system.md b/docs/guide/ui-system.md new file mode 100644 index 0000000..a16b912 --- /dev/null +++ b/docs/guide/ui-system.md @@ -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 +{myController.render()} +``` + +## 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( + ui: IGameUI, + props: UIProps, + 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. 批量处理:一次性完成所有修改 + +### 实际效益 + +- 性能优化:减少像频繁开关灯的资源浪费 +- 流畅保障:避免连续小改动导致的画面闪烁 +- 智能调度:优先处理用户可见区域的变化 diff --git a/docs/guide/ui.md b/docs/guide/ui.md new file mode 100644 index 0000000..669c9b6 --- /dev/null +++ b/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; + +export const MyBook = defineComponent(props => { + return () => ; +}, 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 () => ( + + {/* 文字元素会自动计算长宽,因此不能手动指定 */} + + +); +``` + +## 显示 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(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` 参数,例如: + +```tsx +return () => ( + + {/* 显示绿史莱姆图标,位置在 (32, 32),循环播放动画 */} + + +); +``` + +### 字体 + +我们很多时候也会想要自定义字体,可以通过 `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 () => ( + + + + +); +``` + +更多的字体使用方法可以参考 [API 文档](../api/motajs-render-style/Font) + +### 圆角矩形 + +我们可以为怪物手册的一栏添加圆角矩形,写法如下: + +```tsx +return () => ( + + + +); +``` + +### 线段 + +我们也可以添加线段,作为怪物列表之间的分割线: + +```tsx +return () => ( + + + +); +``` + +### winskin 背景 + +我们可以为手册添加一个 winskin 背景,可以使用 `Background` 组件: + +```tsx +// 从 components 文件夹中引入这个组件 +import { Background } from '../components'; + +return () => ( + + + +); +``` + +### 滚动条 + +怪物多的话一页肯定显示不完,因此我们可以添加一个滚动条 `Scroll` 组件,用法如下: + +```tsx +// 从 components 文件夹中引入这个组件 +import { Scroll } from '../components'; + +return () => ( + // 使用滚动条组件替换 container 元素 + // [!code ++] + + {/* 其他内容 */} + // [!code ++] +); +``` + +在使用滚动条时,建议使用平铺式布局,将每个独立的内容平铺显示,而不是整体包裹为一个 `container`,这有助于提高性能表现。 + +### 循环 + +编写怪物手册的话,我们就必须用到循环,因为我们需要遍历当前怪物列表,然后每个怪物生成一个 `container`,在这个 `container` 里面显示内容。tsx 为我们提供了嵌入表达式的功能,因此我们可以通过 `map` 方法来遍历怪物列表,然后返回一个元素,组成元素数组,实现循环遍历的功能。示例如下: + +```tsx +export const MyBook = defineComponent(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 () => ( + + {/* 写一个 map 循环,将一个容器元素返回,就可以显示了 */} + {enemys.map((v, i) => { + return ( + + {/* 怪物图标与怪物名称 */} + + + {/* 显示怪物的属性 */} + + + {/* 其他的属性,例如攻击,防御等 */} + + ); + })} + + ); +}, myBookProps); +``` + +### 条件判断 + +可以在表达式中使用三元表达式或者立即执行函数来实现条件判断: + +```tsx +return () => ( + + {enemys.length === 0 ? ( + // 无怪物时,显示没有剩余怪物 + // [!code ++] + ) : ( + enemys.map(v => { + // 有怪物时 + }) + )} + +); +``` + +## 响应式 + +使用新的 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; +``` + +然后我们需要在这个参数发生变动时修改怪物列表,可以这么写: + +```tsx +export const MyBook = defineComponent(props => { + // 使用 computed,这样的话就会自动追踪到 props.floorId 参数,更新怪物列表,并更新显示内容 + const enemys = computed(() => core.getCurrentEnemys(props.floorId)); // [!code ++] + + return () => ( + + {/* 需要使用 enemys.value 属性,不能直接使用 enemys.length */} + {enemys.value.length === 0 ? ( // [!code ++] + + ) : ( + // 同上,需要 value 属性 + enemys.value.map(v => {}) // [!code ++] + )} + + ); +}, 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; + +// 这样的话就有响应式效果了 +; +``` + +对于对象类型来说,需要使用 `reactive` 函数包裹,这个函数会把对象变成深层响应式,任何一级发生更改都会触发响应式更新,例如: + +```tsx +const obj = reactive({ obj1: { num: 10 } }); + +// 这个就不需要使用 value 属性了,只有 ref 函数包裹的需要 +obj.obj1.num = 20; + +// 直接调用即可,当值更改时内容也会自动更新 +; +``` + +数组也可以使用 `reactive` 方法来实现响应式: + +```tsx +// 传入一个泛型来指定这个变量的类型,这里使用数字数组作为示例 +const array = reactive([]); + +// 可以使用数组自身的方法添加或修改元素 +array.push(100); + + + {/* 直接对数组遍历,数组修改后这段内容也会自动更新 */} + {array.map(v => ( + + ))} +; +``` + +如果对象比较大,只想让第一层变为响应式,深层的不变,可以使用 `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 属性即可 +{/* 渲染内容 */}; +``` + +可以使用 `cursor` 属性来指定鼠标移动到该元素上时的指针样式,如下例所示,鼠标移动到这个容器上时就会变成小手的形状: + +```tsx + +``` + +鼠标与触摸事件的触发包括两个阶段,从根节点捕获,然后一路传递到最下层,然后从最下层冒泡,然后一路再传递回根节点,一般情况下我们使用冒泡阶段的监听即可,也就是 `onXxx`,例如 `onClick` 等,不过如果我们需要监听捕获阶段的事件,也可以使用 `onXxxCapture` 的方法来监听: + +```tsx +const clickCapture = () => { + console.log('click capture.'); +}; +const click = () => { + console.log('click bubble.'); +}; + +; +``` + +当点击这个容器时,就会先触发 `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'); +}; + + + +; +``` + +在上面这个例子中,当我们点击内层的容器时,只会触发 `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(props => { + // 第一个参数是按键实例,第二个参数是按键作用域,一般用不到 + const [key, scope] = useKey(); + + return () => ; +}); +``` + +最后,实现按键操作,使用 `key.realize` 方法: + +```tsx +import { clamp } from 'lodash-es'; + +export const MyBook = defineComponent(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 () => ; +}); +``` + +## 绘制选择框与动画 + +### 定义选择框动画 + +下面我们来把选择框加上,当按下方向键时,选择框会移动,当按下确定键时,会打开这个怪物的详细信息。首先,我们使用一个描边格式的 `g-rectr` 圆角矩形元素作为选择框: + +```tsx + + + +``` + +接下来,我们需要让它能够移动,当用户按下按键时,选择框会平滑移动到目标位置。这时候,我们可以使用动画接口 `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 + + + +``` + +### 执行动画 + +接下来,我们需要监听当前选中怪物,然后根据当前怪物来设置元素位置,使用 `watch` 监听 `selected` 变量: + +```ts +watch(selected, value => { + // 使用 set 方法来动画至目标值 + rectY.set(16 + value * 80); +}); +``` + +除此之外,我们还可以添加当鼠标移动至怪物元素上时,选择框也移动至目标,我们需要监听 `onEnter` 事件: + +```tsx +const onEnter = (index: number) => { + // 前面已经监听过 selected 了,这里直接设置即可,不需要再调用 rectY.set + // 不过调用了也不会有什么影响,动画会智能处理这种情况 + selected.value = index; +}; + + + {/* 把圆角矩形的纵深调大,防止被怪物容器遮挡 */} + + {enemys.map((v, i) => { + // 元素内容不再展示。监听时,需要传入一个函数,因此需要使用匿名箭头函数包裹, + // 添加 void 关键字是为了防止返回值泄漏,不过在这里并不是必要,因为 onEnter 没有返回值 + return void onEnter(i)}>; + })} +; +``` + +### 处理重叠 + +如果你去尝试着使用上面这个方法来实现动画,并给每个怪物添加了一个点击事件,你会发现你可能无法触发选中怪物的点击事件,这是因为 `g-rectr` 的纵深 `zIndex` 较高,交互事件会传播至此元素,而不会传播至下层元素,于是就不会触发点击事件。样板自然也考虑到了这种情况,我们只需要给圆角矩形添加一个 `noevent` 标识,即可让交互事件不会受到此元素的影响,不过相应地,这个元素上的交互事件也将会无法触发。示例如下: + +```tsx + + + {enemys.map((v, i) => { + return void onEnter(i)}>; + })} + +``` + +## 调用 Scroll 组件接口 + +我们现在已经实现了按键操作,但是移动时并不能同时修改滚动条的位置,这会导致当前选中的怪物跑到画面之外,这时候我们需要自动滚动到目标位置,可以使用 `Scroll` 组件暴露出的接口来实现。我们使用 `ref` 属性来获取其接口: + +```tsx +import { ScrollExpose } from './components'; + +const scrollExpose = ref(); + +; +``` + +然后,我们可以调用其 `scrollTo` 方法来滚动至目标位置: + +```tsx +import { ScrollExpose } from './components'; + +const scrollExpose = ref(); + +watch(selected, () => { + // 滚动到选中怪物上下居中的位置,组件内部会自动处理滚动条边缘,因此不需要担心为负值 + scrollExpose.value.scrollTo(selected.value * 80 - 240); +}); + +; +``` + +## 修改 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({ + floorId: 'MT0', + zIndex: 100 +}); + +mainUIController.open(MyBookUI, props); +``` + +我们可以监听状态栏更新来实时更新参数: + +```ts +import { hook } from '@user/data-base'; + +// 监听状态栏更新事件 +hook.on('updateStatusBar', () => { + // 状态栏更新时,修改怪物手册的楼层为当前楼层 id + props.floorId = core.status.floorId, +}); +``` + +## 总结 + +通过以上的学习,你已经可以做出一个自己的怪物手册了!试着做一下吧! diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..d347f0d --- /dev/null +++ b/docs/index.md @@ -0,0 +1,27 @@ +--- +# https://vitepress.dev/reference/default-theme-home-page +layout: home + +hero: + name: 'mota-js' + text: 'HTML5魔塔样板V2.B' + tagline: HTML5魔塔样板从 2.x 到 3.0 的过渡版本 + actions: + - theme: brand + text: 深度指南 + link: /guide/diff + - theme: alt + text: API列表 + link: /api/index + - theme: alt + text: 旧样板文档 + link: https://h5mota.com/games/template/_docs/#/ + +features: + - title: 高适配性 + details: 不论你是在电脑上,还是手机上,甚至是电视上,游戏机上,只要有一个浏览器,就能游玩HTML5魔塔!不论你使用触屏,还是键盘,不论是鼠标,还是手柄,都能流畅地操作! + - title: 高扩展性 + details: HTML5魔塔样板提供了非常丰富的API,借助于插件API,你可以做出任何你想要的东西! + - title: 上手难度低 + details: 哪怕你不会代码,你也可以轻松地造出一个HTML5魔塔!借助于样板的事件系统与网站插件库,你也可以让你的魔塔更有个性! +--- diff --git a/docs/logger/error/error1.md b/docs/logger/error/error1.md new file mode 100644 index 0000000..f4b0a27 --- /dev/null +++ b/docs/logger/error/error1.md @@ -0,0 +1,212 @@ +# 错误代码一览及其解决方案 1-50 + +## ERROR CODE 1 + +```txt +Unexpected error when posting danmaku. Error info: $1 +``` + +- 报错原因:发送弹幕时发生报错。 +- 解决方案:查看后面的 Error info,检查报错信息内容,按照报错信息修复问题。 + +## ERROR CODE 2 + +```txt +Unexpected loading error in loading resource '$1/$2'. Error info: $3 +``` + +- 报错原因:加载资源时发生报错,可能是资源不存在,或者是网络问题。 +- 解决方案:查看后面的 Error info,检查报错信息内容,按照报错信息修复问题。 + +## ERROR CODE 3 + +```txt +Syntax error in parsing CSS: Unexpected ':'. Col: $1. CSS string: '$2 +``` + +- 报错原因:解析 CSS 时报错,一般是在发送弹幕时引起。 +- 解决方案:检查弹幕 CSS 语法是否正确。 + +## ERROR CODE 4 + +```txt +Syntax error in parsing CSS: Unexpected ';'. Col: $1. CSS string: '$2' +``` + +- 报错原因:解析 CSS 时报错,一般是在发送弹幕时引起。 +- 解决方案:检查弹幕 CSS 语法是否正确。 + +## ERROR CODE 5 + +```txt +Syntax error in parsing CSS: Missing property name after '-'. Col: $1. CSS string: '$2' +``` + +- 报错原因:解析 CSS 时报错,一般是在发送弹幕时引起。 +- 解决方案:检查弹幕 CSS 语法是否正确。 + +## ERROR CODE 6 + +```txt +Syntax error in parsing CSS: Unexpected end of css, expecting ':'. Col: $1. CSS string: '$2' +``` + +- 报错原因:解析 CSS 时报错,一般是在发送弹幕时引起。 +- 解决方案:检查弹幕 CSS 语法是否正确。 + +## ERROR CODE 7 + +```txt +Syntax error in parsing CSS: Unexpected end of css, expecting property value. Col: $1. CSS string: '$2' +``` + +- 报错原因:解析 CSS 时报错,一般是在发送弹幕时引起。 +- 解决方案:检查弹幕 CSS 语法是否正确。 + +## ERROR CODE 8 + +```txt +Post danmaku with not allowed css. Info: $1 +``` + +- 报错原因:弹幕 CSS 中使用了不允许的 css 属性类型。 +- 解决方案:目前仅支持 `color` `background-color` `font-size: x%` 属性。 + +## ERROR CODE 9 + +```txt +Cannot initialize shader program. Error info: $1 +``` + +- 报错原因:不能够初始化着色器脚本,可能是着色器代码中有语法错误,或者是定义了不存在的变量等。 +- 解决方案:查看报错内容,根据报错内容解决。 + +## ERROR CODE 10 + +```txt +Cannot compile $1 shader. Error info: $2 +``` + +- 报错原因:不能编译着色器脚本,可能是语法错误,设备不支持 OpenGL 等原因。 +- 解决方案:查看报错内容,根据报错内容解决。 + +## ERROR CODE 11 + +```txt +Cache depth cannot larger than 31. +``` + +- 报错原因:`BlockCache` 最大允许 31 层深度缓存。 +- 解决方案:降低缓存深度。 + +## ERROR CODE 12 + +```txt +Cannot move while status is not 'moving'. Call 'readyMove' first. +``` + +- 报错原因:调用移动时没有调用 `readyMove` 准备移动。 +- 解决方案:在移动前先调用 `readyMove`。 + +## ERROR CODE 13 + +见 [CODE 10](#error-code-10) + +## ERROR CODE 16 + +```txt +Cannot find log message for $1 code $2. +``` + +- 报错原因:不能找到错误代码 $2 的消息。 +- 解决方案:避免使用 `logger` 输出不存在的错误代码。 + +## ERROR CODE 17 + +```txt +Cannot use shader program for shader element that does not belong to it. +``` + +- 报错原因:在一个着色器上使用了不属于这个着色器的着色器程序。 +- 解决方案:确保使用的着色器程序是由着色器对象自身创建的。 + +## ERROR CODE 18 + +```txt +Cannot delete shader program for shader element that does not belong to +``` + +- 报错原因:在一个着色器上删除了不属于这个着色器的着色器程序。 +- 解决方案:确保删除的着色器程序是由着色器对象自身创建的。 + +## ERROR CODE 19 + +```txt +Cannot create MotaRenderer instance for nonexistent canvas. +``` + +- 报错原因:在一个不存在的画布上创建了渲染器对象。 +- 解决方案:确保目标画布存在。 + +## ERROR CODE 20 + +```txt +Cannot create render element for tag '$1', since there's no registration for it. +``` + +- 报错原因:不能创建 $1 标签,因为没有注册这个标签。 +- 解决方案:确保你已经在 `tagMap` 注册了这个标签。 + +## ERROR CODE 21 + +```txt +Incorrect render prop type is delivered. key: '$1', expected type: '$2', delivered type: '$3' +``` + +- 报错原因:向元素中传入了错误类型的参数(props)。 +- 解决方案:确保传入元素的 $1 参数的类型是 $2。 + +## ERROR CODE 22 + +```txt +Incorrect props for custom tag. Please ensure you have delivered 'item' prop and other required props. +``` + +- 报错原因:没有向 `cutsom` 标签传入 `item` 参数。 +- 解决方案:确保传入了 `item` 参数和需要的所有参数。 + +## ERROR CODE 23 + +```txt +Cannot get reader when fetching '$1'. +``` + +- 报错原因:流式加载 URL $1 时不能获取 `StreamReader`。 +- 解决方案:检查加载的 URL 是否合法,检查浏览器版本是否过老。 + +## ERROR CODE 24 + +```txt +Cannot decode source type of '$1', since there is no registered decoder for that type. +``` + +- 报错原因:音频系统中的流式音频源不能解析 $1 格式的音频,因为没有对应的解码器。 +- 解决方案:如果不是 `opus` `ogg` 格式的音频,请使用 `ElementSource`。 + +## ERROR CODE 25 + +```txt +Unknown audio type. Header: '$1' +``` + +- 报错原因:未知的音频类型。 +- 解决方案:目前仅支持 `mp3` `wav` `flac` `opus` `ogg` `aac` 格式的音频。 + +## ERROR CODE 26 + +```txt +Uncaught error when fetching stream data from '$1'. Error info: $2. +``` + +- 报错原因:流式加载时报错。 +- 解决方案:查看报错内容,根据报错内容解决问题。 diff --git a/docs/logger/index.md b/docs/logger/index.md new file mode 100644 index 0000000..a87e9b9 --- /dev/null +++ b/docs/logger/index.md @@ -0,0 +1,3 @@ +# 错误代码一览及解决方案 + +请在左侧导航栏查找你想查找的错误或警告代码。 diff --git a/docs/logger/warn/warn1.md b/docs/logger/warn/warn1.md new file mode 100644 index 0000000..6ef9683 --- /dev/null +++ b/docs/logger/warn/warn1.md @@ -0,0 +1,445 @@ +# 警告代码一览及其解决方案 1-50 + +## WARN CODE 1 + +```txt +Resource with type of 'none' is loaded. +``` + +- 警告原因:不能加载 `none` 类型的资源。 +- 解决方案:加载资源时填写资源类型。 + +## WARN CODE 2 + +```txt +Repeat load of resource '$1/$2'. +``` + +- 警告原因:同一个资源被加载了两次。 +- 解决方案:避免对同一个资源调用两次加载。 + +## WARN CODE 3 + +```txt +Unknown danmaku tag: $1 +``` + +- 警告原因:出现了未知的弹幕标签(指 `[xxx:xxx]`) +- 解决方案:目前仅支持 `[i:xxx]` 标签,如果需要显示方括号,请使用 `\[\]`。 + +## WARN CODE 4 + +```txt +Ignored a mismatched ']' in danmaku. +``` + +- 警告原因:出现了不能匹配的右方括号。 +- 解决方案:如果需要显示方括号,请使用 `\[\]`。 + +## WARN CODE 5 + +```txt +Repeat post danmaku. +``` + +- 警告原因:同一个弹幕被发送了两次。 +- 解决方案:确保一个弹幕实例只调用了一次 `post` 方法。 + +## WARN CODE 6 + +```txt +Registered special danmaku element: $1. +``` + +- 警告原因:要注册的弹幕标签已经存在。 +- 解决方案:避免使用同一个标签名,如果内容不一样请换一个标签名。 + +## WARN CODE 7 + +参考 [CODE 3](#warn-code-3) + +## WARN CODE 8 + +```txt +Incomplete render data is put. None will be filled to the lacked data. +``` + +- 警告原因:使用 `` 标签时,向地图渲染传入了不完整的地图信息(数据长度不是指定宽度的倍数),不完整的部分将会补零。 +- 解决方案:确保传入的地图信息长度是 `width` 参数的倍数。 + +## WARN CODE 9 + +```txt +Data transfered is partially (or totally) out of range. Overflowed data will be ignored. +``` + +- 警告原因:使用 `` 标签时,传入地图的渲染数据有一部分(或全部都)在地图之外,在地图之外的部分将会被忽略。 +- 解决方案:确保传入的地图信息没有在地图之外的部分。 + +## WARN CODE 10 + +```txt +Cannot resolve big image of enemy '$1'. +``` + +- 警告原因:无法解析怪物 $1 的大怪物贴图(绑定贴图),可能是因为图片不存在。 +- 解决方案:确保此怪物绑定的贴图已经在全塔属性注册。 + +## WARN CODE 11 + +```txt +Cannot resolve material $1. Material not exists. +``` + +- 警告原因:不能解析指定类型 $1 的素材,因为对应的素材文件不存在。 +- 解决方案:检查 `enemys.png` `npcs.png` 等素材文件是否存在。 + +## WARN CODE 12 + +```txt +Cannot mark buffable with a non-number status. Key: '$1'. +``` + +- 暂时碰不到这个报错。 + +## WARN CODE 13 + +```txt +Cannot set buff of non-number status. Key: '$1'. +``` + +- 暂时碰不到这个报错。 + +## WARN CODE 14 + +```txt +Cannot add status of non-number status. Key: '$1'. +``` + +- 暂时碰不到这个报错。 + +## WARN CODE 15 + +```txt +Cannot get item of a non-item block on loc: $1,$2,$3. +``` + +- 警告原因:不能获取一个不存在物品的图块上的物品对象。 +- 解决方案:提前判断那一格是不是物品,或确保要获取的格子包含物品。 + +## WARN CODE 16 + +```txt +Override repeated state key: '$1'. +``` + +- 暂时碰不到这个报错。 + +## WARN CODE 17 + +```txt +Floor-damage extension needs 'floor-binder' extension as dependency. +``` + +- 警告原因:楼层伤害拓展需要以楼层绑定拓展作为依赖。 +- 解决方案:确保添加伤害拓展时也添加了楼层绑定拓展。 + +## WARN CODE 18 + +```txt +Uncaught error in posting like info for danmaku. Danmaku id: $1. +``` + +- 警告原因:为弹幕点赞时出现报错。 +- 解决方案:可能是网络问题,检查网络。 + +## WARN CODE 19 + +```txt +Repeat light id: '$1' +``` + +- 警告原因:重复的光源 id。 +- 解决方案:避免光源 id 出现重复。 + +## WARN CODE 20 + +```txt +Cannot apply animation to camera operation that does not belong to it. +``` + +- 警告原因:不能向摄像机对象添加不属于它的动画操作。 +- 解决方案:确保添加的动画操作是由这个摄像机对象创建的。 + +## WARN CODE 21 + +```txt +Cannot apply transition to camera operation that does not belong to it. +``` + +- 警告原因:不能向摄像机对象添加不属于它的渐变操作。 +- 解决方案:确保添加的渐变操作是由这个摄像机对象创建的。 + +## WARN CODE 22 + +```txt +There is already an active camera for delivered render item. Consider using 'Camera.for' or disable the active camera to avoid some exceptions. +``` + +- 警告原因:在目标渲染元素上,现在已经有了一个已激活的摄像机对象,这可能导致两个摄像机操作冲突,产生问题。 +- 解决方案:考虑使用 [`Camera.for`](../../api/motajs-render-elements/Camera.md#Camera.for) 方法,或先禁用已激活的摄像机,再使用当前摄像机 + +## WARN CODE 23 + +```txt +Render item with id of '$1' has already exists. Please avoid repeat id since it may cause issues when use 'getElementById'. +``` + +- 警告原因:两个渲染元素的 id 出现了重复,这会导致调用 `getElementById` 时出现问题。 +- 解决方案:避免出现重复的 id。 + +## WARN CODE 24 + +```txt +Uniform block can only be used in glsl version es 300. +``` + +- 警告原因:UBO(Uniform Block Object) 只能在 GLSL ES 300 版本的着色器脚本中使用。 +- 解决方案:如果需要使用 UBO,考虑换用 es 300 版本的着色器脚本。 + +## WARN CODE 25 + +```txt +Cannot activate weather since there's no weather with id of '$1'. +``` + +- 警告原因:不能启用不存在的天气类型。 +- 解决方案:确保要启用的天气类型正确且存在,不存在则需要自行注册。 + +## WARN CODE 26 + +```txt +Cannot set attribute when only element number specified. Use 'pointer' or 'pointerI' instead. +``` + +- 警告原因:使用 `defineAttribute` 时指定了不存在的顶点属性类型。 +- 解决方案:如果需要传递数组,考虑使用 `defineAttribArray` 而不是 `defineAttribute`。 + +## WARN CODE 27 + +```txt +Cannot vertex attribute integer point when specified as float. Use 'set' or 'pointer' instead. +``` + +- 遇不到这个报错 + +## WARN CODE 28 + +```txt +Redefinition of shader $1: '$2' +``` + +- 警告原因:定义了重复的着色器变量/顶点属性/UBO 等。 +- 解决方案:避免对同一个变量调用多次 `defineXxxx`。 + +## WARN CODE 29 + +```txt +Cannot define new texture since texture index is larger than max texture count. +``` + +- 警告原因:定义的纹理数量超过了设备支持的上限。 +- 解决方案:考虑将多个纹理合并为同一个纹理作为图集,然后使用顶点属性或一致变量进行裁剪。 + +## WARN CODE 30 + +```txt +Cannot use indices named $1 since no definition for it. Please define it in advance. +``` + +- 警告原因:要作为顶点索引的索引数组不存在,因为没有定义。 +- 解决方案:提前定义索引数组。 + +## WARN CODE 31 + +```txt +Cannot use indices since the indices instance is not belong to the program. +``` + +- 警告原因:使用的顶点索引数组不属于当前着色器程序。 +- 解决方案:确保使用的顶点索引数组是由当前着色器程序创建的。 + +## WARN CODE 32 + +```txt +Sub-image exceeds texture dimensions, auto adjusting size. +``` + +- 警告原因:使用 `IShaderTexture.sub` 时,传入的图像数据超出了纹理大小。 +- 解决方案:确保传入的图片不会超出纹理大小。如果需要修改纹理大小,请使用 `IShaderTexture.set` 方法。 + +## WARN CODE 33 + +```txt +Cannot modify MotaOffscreenCanvas2D that is freezed. +``` + +- 警告原因:不能修改已冻结的画布属性。 +- 解决方案:如果这个画布后续还需要修改属性,那么就不要冻结它。 + +## WARN CODE 34 + +```txt +Repeated render tag registration: '$1'. +``` + +- 警告原因:注册了重复的渲染标签。 +- 解决方案:确保注册的渲染标签名称不重复。 + +## WARN CODE 35 + +```txt +Cannot append child on plain render item, please ensure you have overrided 'appendChild' method in your own element. +``` + +- 警告原因:默认的渲染元素中,只有一部分可以添加子元素,而其他的不能添加。 +- 解决方案:不要在不能添加子元素的元素里面添加子元素。如果是自定义元素,请确保实现了 `appendChild` 方法。 + +## WARN CODE 36 + +```txt +Cannot remove child on plain render item, please ensure you have overrided 'removeChild' method in your own element. +``` + +- 警告原因:默认的渲染元素中,只有一部分可以移除子元素,而其他的不能移除。 +- 解决方案:不要在不能移除子元素的元素里面移除子元素。如果是自定义元素,请确保实现了 `removeChild` 方法。 + +## WARN CODE 37 + +```txt +Cannot execute 'requestSort' on plain render item, please ensure you have overrided 'requestSort' method in your own element. +``` + +- 警告原因:默认的渲染元素中,只有一部分可以拥有排序功能,而其他的不能排序。 +- 解决方案:不要在不能拥有子元素的元素上调用 `requestSort`。如果是自定义元素,请确保实现了 `requestSort` 方法。 + +## WARN CODE 38 + +```txt +Using plain text in jsx is strongly not recommended, since you can hardly modify its attributes. Consider using Text element instead. +``` + +- 警告原因:在 JSX 中直接填写文字内容是极其不推荐的,因为你几乎不能修改它的任何属性。 +- 解决方案:考虑使用 `text` 标签替代。 + +## WARN CODE 39 + +```txt +Plain text is not supported outside Text element. +``` + +- 警告原因:不能在 `text` 元素外使用变量作为文字,例如: + +```tsx +{text.value} +``` + +- 解决方案:换用 `text` 标签。 + +## WARN CODE 40 + +```txt +Cannot return canvas that is not provided by this pool. +``` + +- 遇不到这个报错。 + +## WARN CODE 41 + +```txt +Width of text content components must be positive. receive: $1 +``` + +- 警告原因:`TextContent` 组件的宽度必须是正值,而你可能传入了一个负值或 0。 +- 解决方案:确保宽度属性是正值。 + +## WARN CODE 42 + +```txt +Repeated Textbox id: '$1'. +``` + +- 警告原因:`Textbox` 组件使用了重复的 id。 +- 解决方案:避免 `Textbox` 组件的 id 重复。 + +## WARN CODE 43 + +```txt +Cannot set icon of '$1', since it does not exists. Please ensure you have delivered correct icon id or number. +``` + +- 警告原因:向 `icon` 元素中传入了不存在的图标。 +- 解决方案:确保传入的图标 id 或数字是正确的。 + +## WARN CODE 44 + +```txt +Unexpected end when loading stream audio, reason: '$1' +``` + +- 警告原因:加载流式音频时被意外中断。 +- 解决方案:根据原因解决。 + +## WARN CODE 45 + +```txt +Audio route with id of '$1' has already existed. New route will override old route. +``` + +- 警告原因:id 为 $1 的音频路由已经存在,新的路由将会覆盖旧路由。 +- 解决方案:确保音频路由不会重复。 + +## WARN CODE 46 + +```txt +Cannot pipe new StreamReader object when stream is loading. +``` + +- 警告原因:在流式加载过程中无法将流加载对象泵入其他对象。 +- 解决方案:在流式加载前就执行 `pipe` 方法。 + +## WARN CODE 47 + +```txt +Audio stream decoder for audio type '$1' has already existed. +``` + +- 警告原因:$1 类型的音频解码器已经存在。 +- 解决方案:不要为同一种类型的音频注册多种解码器。 + +## WARN CODE 48 + +```txt +Sample rate in stream audio must be constant. +``` + +- 警告原因:流式音频中,音频的采样率应该保持一致。 +- 解决方案:确保音频的采样率不会改变,如果会的话,请换一个音频。 + +## WARN CODE 49 + +```txt +Repeated patch for '$1', key: '$2'. +``` + +- 警告原因:对同一个 2.x 样板接口重写了两次。 +- 解决方案:将两次重写合并为一次。 + +## WARN CODE 50 + +```txt +Unknown audio extension name: '$1' +``` + +- 警告原因:未知的文件拓展名 $1。 +- 解决方案:换一个类型的音频。 diff --git a/docs/logger/warn/warn2.md b/docs/logger/warn/warn2.md new file mode 100644 index 0000000..b1bf500 --- /dev/null +++ b/docs/logger/warn/warn2.md @@ -0,0 +1,118 @@ +# 警告代码一览及其解决方案 51-100 + +## WARN CODE 51 + +```txt +Cannot decode sound '$1', since audio file may not supported by 2.b. +``` + +- 警告原因:不能解码音效 $1,可能因为文件类型不支持。 +- 解决方案:换一个音乐文件格式,建议使用 `opus` 格式。 + +## WARN CODE 52 + +```txt +Cannot play sound '$1', since there is no added data named it. +``` + +- 警告原因:播放了不存在的音效。 +- 解决方案:确保要播放音效名称正确。 + +## WARN CODE 53 + +```txt +Cannot $1 audio route '$2', since there is not added route named it. +``` + +- 警告原因:不能对 $2 路由执行 $1(播放、暂停等)操作,因为没有名字叫这个的音频路由。 +- 解决方案:确保要操作的路由存在。 + +## WARN CODE 54 + +```txt +Missing start tag for '$1', index: $2. +``` + +- 警告原因:在 `TextContent` 组件中,匹配不到转义字符 $1 的起始位置。 +- 解决方案:确保每个转义字符包含起始标签。 + +## WARN CODE 55 + +```txt +Unchildable tag '$1' should follow with param. +``` + +- 警告原因:在 `TextContent` 组件中,没有子标签的转义字符(`\i`)后面必须跟着参数。 +- 解决方案:确保 `\i` 后面紧跟着图标名称,例如 `\i[greenSlime]`。 + +## WARN CODE 56 + +```txt +Method '$1' has been deprecated. Consider using '$2' instead. +``` + +- 警告原因:接口 $1 已经弃用。 +- 解决方案:考虑换为 $2。 + +## WARN CODE 57 + +```txt +Repeated UI controller id '$1'. +``` + +- 警告原因:重复的 UI 控制器 id。 +- 解决方案:避免 UI 控制器的 id 重复。 + +## WARN CODE 58 + +```txt +Fail to set ellipse round rect, since length of 'ellipse' property should only be 2, 4, 6 or 8. delivered: $1 +``` + +- 警告原因:椭圆模式的圆角矩形传入的参数数组需要是 2,4,6,8 长度,而传入了 $1 长度的数组。 +- 解决方案:确保传入参数正确。 + +## WARN CODE 59 + +```txt +Unknown icon '$1' in parsing text content. +``` + +- 警告原因:在 `TextContent` 中出现了未知的图标。 +- 解决方案:确保 `\i` 后的图标参数正确。 + +## WARN CODE 60 + +```txt +Repeated Tip id: '$1'. +``` + +- 警告原因:`Tip` 组件的 id 重复。 +- 解决方案:避免 id 重复。 + +## WARN CODE 61 + +```txt +Unexpected recursive call of $1.update?$2 in render function. Please ensure you have to do this, if you do, ignore this warn. +``` + +- 警告原因:在渲染元素的渲染函数中出现了递归 `update` 调用,这会导致元素一直更新而且难以察觉,同时也会引起性能问题。 +- 解决方案:避免在渲染函数中调用 `update` 方法。如果你必须这么做,请忽视这个警告。 + +## WARN CODE 62 + +```txt +Recursive fallback fonts in '$1'. +``` + +- 警告原因:字体回退出现了循环。例如 `font1` -> `font2` -> `font1`。 +- 解决方案:避免出现循环字体回退。 + +## WARN CODE 63 + +```txt +Uncaught promise error in waiting box component. Error reason: $1 +``` + +- 警告原因:在等待 box 组件(选择框、确认框等)时,出现了异步报错。 +- 解决方案:根据报错内容解决问题。 diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..425f8f0 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,80 @@ +import eslint from '@eslint/js'; +import globals from 'globals'; +import tseslint from 'typescript-eslint'; +import eslintPluginVue from 'eslint-plugin-vue'; +import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; +import eslintPluginReact from 'eslint-plugin-react'; + +export default tseslint.config( + { + ignores: ['node_modules', 'dist', 'public'] + }, + + eslint.configs.recommended, + ...tseslint.configs.recommended, + ...eslintPluginVue.configs['flat/recommended'], + eslintPluginPrettierRecommended, + { + files: ['**/*.{js,mjs,cjs,vue}'], + rules: { + 'no-console': 'warn', + eqeqeq: ['error', 'always'] + } + }, + { + languageOptions: { + globals: { + ...globals.browser, + wx: true + } + } + }, + { + files: ['**/*.vue'], + languageOptions: { + parserOptions: { + parser: tseslint.parser, + ecmaVersion: 'latest', + ecmaFeatures: { + jsx: true + } + } + }, + rules: { + 'vue/no-mutating-props': [ + 'error', + { + shallowOnly: true + } + ] + } + }, + { + files: ['**/*.{ts,tsx,vue}'], + plugins: { + react: eslintPluginReact + }, + rules: { + '@typescript-eslint/no-empty-object-type': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-unused-vars': [ + 'error', + { + args: 'all', + argsIgnorePattern: '^_', + caughtErrors: 'all', + caughtErrorsIgnorePattern: '^_', + destructuredArrayIgnorePattern: '^_', + varsIgnorePattern: '^_', + ignoreRestSiblings: true + } + ], + '@typescript-eslint/no-namespace': 'off', + '@typescript-eslint/no-this-alias': 'off', + 'no-console': 'warn', + 'vue/multi-word-component-names': 'off', + eqeqeq: ['error', 'always'], + 'react/jsx-boolean-value': ['error', 'never'] + } + } +); diff --git a/idea.md b/idea.md deleted file mode 100644 index c4adc47..0000000 --- a/idea.md +++ /dev/null @@ -1,57 +0,0 @@ -## 怪物 - -### 第二章 智慧 - -- 同化 -- 同化+阻击 -- 电摇嘲讽:到同行或同列直接怼过去,门和墙撞碎,不消耗钥匙,攻击怪物,捡道具,改变 bgm,可吃补给用 -- 乾坤挪移:平移光环位置 -- 加光环的光环 - -#### Boss - -音游,音乐为一个被遗忘的夜晚,可选简单与困难,困难可获得成就冰与火之舞 - -玩法:一个会转动的圆盘,带有一个伸出去的把手,boss 从四面八方射子弹,当子弹恰好落到把手前端时,点击按键或屏幕,可以抵挡子弹,简单难度 3 条命,困难 1 条。简单难度音符密度低。困难难度为冰与火之舞的节奏。简单难度判定时间为前后各 100ms,困难为 50ms - -## 主角 - -- 学习:学习选定怪物的选定技能(不能学光环),消耗智慧点,初始 400,但每次消耗点数+100,持续 3 场战斗 -- 铸剑为盾:主动技能,减少攻击,增加防御 -- 血之代偿:消耗一定血量,战前对怪物造成同等数值的伤害 - -### 第三章 战争 - -#### 技能 - -闪避:每 M 回合闪避一次,减少 N% 的伤害 - -## 机制 - -### 通用 - -- 实时天气 -- 成就系统(完成) -- 装备合成、装备(孔)强化 -- 宝石目标设定 -- 自动宝物规划,选中两个或更多宝物后自动在本地图中规划出最优拾取路线,原则是尽量减少其余宝物的捡拾,自动切换主动技能,怪物造成的伤害最低的路线 -- 临界显示方式,宝石数还是数值 -- 怪物目标设定(完成) -- 木牌查看系统(完成) -- 宝物目标设定 -- 每个怪物加一个怪物说明 -- 歌词展示系统 - -### 第二章 智慧 - -- 苍蓝之殿 1: 利用点光源,照到的位置与没照到的位置内容不同,玩家可以选择是否装备手电筒 -- 苍蓝之殿 2: - -## 成就 - -- 虚惊一场:打完山洞门口的怪只剩 1 滴血 -- 学坏了:学习敌人的电摇嘲讽技能 -- 真能刷:勇气之路的刷血怪刷到 15w 以上的血 -- 满腹经纶:把所有能学的怪物技能都学一遍 -- 冰与火之舞:通过第二章特殊战的困难难度 -- 你是怎么做到的?!:山路地图与勇气之路地图中与若干个神秘的木牌对话 diff --git a/index.html b/index.html index 6db8448..5be5cff 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ - + HTML5魔塔 @@ -19,161 +19,14 @@ - - - - - + - - - 请稍候... - - - - - - 资源即将开始加载 - HTML5魔塔游戏平台,享受更多魔塔游戏:https://h5mota.com/ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 此浏览器不支持HTML5 - - + + + + 此浏览器不支持HTML5 + + @@ -185,7 +38,6 @@ - diff --git a/mota.config.ts b/mota.config.ts index fdfa1a1..bc6c2a4 100644 --- a/mota.config.ts +++ b/mota.config.ts @@ -1,8 +1,7 @@ interface MotaConfig { name: string; - /** 资源分组打包信息 */ - resourceZip?: string[][]; resourceName?: string; + zip?: Record; } function defineConfig(config: MotaConfig): MotaConfig { @@ -12,5 +11,31 @@ function defineConfig(config: MotaConfig): MotaConfig { export default defineConfig({ // 这里修改塔的name,请保持与全塔属性的完全相同,否则发布之后可能无法进行游玩 name: 'HumanBreak', - resourceName: 'HumanBreakRes' + resourceName: 'HumanBreakRes', + zip: { + 'resource.zip': [ + 'autotiles/*', + 'tilesets/*', + 'images/*', + 'animates/*', + 'sounds/*', + 'fonts/*', + '!images/bg.jpg' + ], + 'weather.zip': [ + 'materials/fog.png', + 'materials/cloud.png', + 'materials/sun.png' + ], + 'materials.zip': [ + 'materials/animates.png', + 'materials/enemy48.png', + 'materials/enemys.png', + 'materials/icons.png', + 'materials/items.png', + 'materials/npc48.png', + 'materials/npcs.png', + 'materials/terrains.png' + ] + } }); diff --git a/package.json b/package.json index 000963d..9ca554b 100644 --- a/package.json +++ b/package.json @@ -1,63 +1,92 @@ { - "name": "mota-ts", - "private": true, - "version": "1.0.0", - "type": "module", - "scripts": { - "dev": "ts-node-esm script/dev.ts", - "build": "vue-tsc && vite build && ts-node-esm script/build.ts 0 1 1", - "build-gh": "vue-tsc && vite build --base=/HumanBreak/ && ts-node-esm script/build.ts 1", - "build-local": "vue-tsc && vite build --base=/ && ts-node-esm script/build.ts 1", - "preview": "vite preview", - "update": "ts-node-esm script/update.ts", - "declare": "ts-node-esm script/declare.ts", - "type": "vue-tsc --noEmit", - "lines": "ts-node-esm script/lines.ts" - }, - "dependencies": { - "@ant-design/icons-vue": "^6.1.0", - "ant-design-vue": "^3.2.20", - "axios": "^1.4.0", - "chart.js": "^4.3.0", - "jszip": "^3.10.1", - "lodash-es": "^4.17.21", - "lz-string": "^1.5.0", - "mutate-animate": "^1.1.1", - "three": "^0.149.0", - "vue": "^3.3.4" - }, - "devDependencies": { - "@babel/cli": "^7.21.5", - "@babel/core": "^7.21.8", - "@babel/preset-env": "^7.21.5", - "@rollup/plugin-babel": "^6.0.3", - "@rollup/plugin-commonjs": "^25.0.0", - "@rollup/plugin-node-resolve": "^15.0.2", - "@rollup/plugin-replace": "^5.0.2", - "@rollup/plugin-terser": "^0.4.3", - "@rollup/plugin-typescript": "^11.1.1", - "@types/babel__core": "^7.20.0", - "@types/fontmin": "^0.9.0", - "@types/fs-extra": "^9.0.13", - "@types/lodash-es": "^4.17.7", - "@types/node": "^18.16.14", - "@types/ws": "^8.5.4", - "@vitejs/plugin-legacy": "^4.0.3", - "@vitejs/plugin-vue": "^4.2.3", - "@vitejs/plugin-vue-jsx": "^3.0.1", - "chokidar": "^3.5.3", - "compressing": "^1.9.0", - "fontmin": "^0.9.9", - "form-data": "^4.0.0", - "fs-extra": "^10.1.0", - "less": "^4.1.3", - "rollup": "^3.23.0", - "terser": "^5.17.6", - "ts-node": "^10.9.1", - "typescript": "^4.9.5", - "unplugin-vue-components": "^0.22.12", - "vite": "^4.3.8", - "vue-tsc": "^1.6.5", - "ws": "^8.13.0" - } -} \ No newline at end of file + "name": "mota-ts", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "tsx script/dev.ts", + "build": "vue-tsc && vite build && tsx script/build.ts dist", + "build-local": "vue-tsc && vite build && tsx script/build.ts local", + "preview": "vite preview", + "update": "tsx script/update.ts", + "declare": "tsx script/declare.ts", + "type": "vue-tsc --noEmit", + "lines": "tsx script/lines.ts packages packages-user", + "build:packages": "vue-tsc --noEmit && tsx script/build-packages.ts", + "build:game": "vue-tsc --noEmit && tsx script/build-game.ts", + "build:lib": "vue-tsc --noEmit && tsx script/build-lib.ts", + "docs:dev": "concurrently -k -n SIDEBAR,VITEPRESS -c blue,green \"tsx docs/.vitepress/api.ts\" \"vitepress dev docs\"", + "docs:build": "vitepress build docs", + "docs:preview": "vitepress preview docs" + }, + "dependencies": { + "@ant-design/icons-vue": "^6.1.0", + "@wasm-audio-decoders/ogg-vorbis": "^0.1.16", + "anon-tokyo": "0.0.0-alpha.0", + "ant-design-vue": "^3.2.20", + "axios": "^1.8.4", + "chart.js": "^4.4.8", + "codec-parser": "^2.5.0", + "eventemitter3": "^5.0.1", + "gl-matrix": "^3.4.3", + "jszip": "^3.10.1", + "lodash-es": "^4.17.21", + "lz-string": "^1.5.0", + "mutate-animate": "^1.4.2", + "ogg-opus-decoder": "^1.6.14", + "opus-decoder": "^0.7.7", + "vue": "^3.5.17" + }, + "devDependencies": { + "@babel/cli": "^7.26.4", + "@babel/core": "^7.26.10", + "@babel/preset-env": "^7.26.9", + "@eslint/js": "^9.24.0", + "@rollup/plugin-babel": "^6.0.4", + "@rollup/plugin-commonjs": "^25.0.8", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^15.3.1", + "@rollup/plugin-replace": "^5.0.7", + "@rollup/plugin-terser": "^0.4.4", + "@rollup/plugin-typescript": "^11.1.6", + "@types/babel__core": "^7.20.5", + "@types/fontmin": "^0.9.5", + "@types/fs-extra": "^9.0.13", + "@types/lodash-es": "^4.17.12", + "@types/node": "^18.19.80", + "@types/ws": "^8.18.0", + "@vitejs/plugin-legacy": "^6.0.2", + "@vitejs/plugin-vue": "^5.2.3", + "@vitejs/plugin-vue-jsx": "^4.1.2", + "chokidar": "^3.6.0", + "compressing": "^1.10.1", + "concurrently": "^9.1.2", + "eslint": "^9.22.0", + "eslint-plugin-prettier": "^5.5.0", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-vue": "^9.33.0", + "fontmin": "^0.9.9", + "form-data": "^4.0.2", + "fs-extra": "^10.1.0", + "glob": "^11.0.1", + "globals": "^15.15.0", + "less": "^4.2.2", + "madge": "^8.0.0", + "markdown-it-mathjax3": "^4.3.2", + "mermaid": "^11.5.0", + "postcss-preset-env": "^9.6.0", + "prettier": "^3.5.3", + "rollup": "^3.29.5", + "terser": "^5.39.0", + "tsx": "^4.19.3", + "typescript": "^5.8.2", + "typescript-eslint": "^8.27.0", + "unplugin-vue-components": "^0.22.12", + "vite": "^6.2.2", + "vite-plugin-dts": "^4.5.4", + "vitepress": "^1.6.3", + "vitepress-plugin-mermaid": "^2.0.17", + "vue-tsc": "^2.2.8", + "ws": "^8.18.1" + } +} diff --git a/packages-user/client-modules/package.json b/packages-user/client-modules/package.json new file mode 100644 index 0000000..3a9c7b5 --- /dev/null +++ b/packages-user/client-modules/package.json @@ -0,0 +1,17 @@ +{ + "name": "@user/client-modules", + "dependencies": { + "@motajs/client-base": "workspace:*", + "@motajs/common": "workspace:*", + "@motajs/render": "workspace:*", + "@motajs/render-core": "workspace:*", + "@motajs/legacy-common": "workspace:*", + "@motajs/legacy-ui": "workspace:*", + "@motajs/types": "workspace:*", + "@motajs/system-action": "workspace:*", + "@motajs/system-ui": "workspace:*", + "@user/data-base": "workspace:*", + "@user/data-state": "workspace:*", + "@user/legacy-plugin-data": "workspace:*" + } +} diff --git a/packages-user/client-modules/src/action/hotkey.ts b/packages-user/client-modules/src/action/hotkey.ts new file mode 100644 index 0000000..97ef6ee --- /dev/null +++ b/packages-user/client-modules/src/action/hotkey.ts @@ -0,0 +1,613 @@ +import { KeyCode } from '@motajs/client-base'; +import { gameKey, HotkeyJSON } from '@motajs/system-action'; +import { hovered, mainUi, tip, openDanmakuPoster } from '@motajs/legacy-ui'; +import { GameStorage } from '@motajs/legacy-system'; + +export const mainScope = Symbol.for('@key_main'); + +// todo: 读取上一个手动存档,存档至下一个存档栏 +// ----- Register +gameKey + //#region 游戏按键 + .group('game', '游戏按键') + .register({ + id: 'moveUp', + name: '上移', + defaults: KeyCode.UpArrow + }) + .register({ + id: 'moveDown', + name: '下移', + defaults: KeyCode.DownArrow + }) + .register({ + id: 'moveLeft', + name: '左移', + defaults: KeyCode.LeftArrow + }) + .register({ + id: 'moveRight', + name: '右移', + defaults: KeyCode.RightArrow + }) + //#region ui界面 + .group('ui', 'ui界面') + .register({ + id: 'book', + name: '怪物手册', + defaults: KeyCode.KeyX + }) + .register({ + id: 'save', + name: '存档界面', + defaults: KeyCode.KeyS + }) + .register({ + id: 'load', + name: '读档界面', + defaults: KeyCode.KeyD + }) + .register({ + id: 'toolbox', + name: '道具栏', + defaults: KeyCode.KeyT + }) + .register({ + id: 'equipbox', + name: '装备栏', + defaults: KeyCode.KeyQ + }) + .register({ + id: 'fly', + name: '楼层传送', + defaults: KeyCode.KeyG + }) + .register({ + id: 'menu', + name: '菜单', + defaults: KeyCode.Escape + }) + .register({ + id: 'replay', + name: '录像回放', + defaults: KeyCode.KeyR + }) + .register({ + id: 'shop', + name: '快捷商店', + defaults: KeyCode.KeyV + }) + .register({ + id: 'statistics', + name: '数据统计', + defaults: KeyCode.KeyB + }) + .register({ + id: 'viewMap_1', + name: '浏览地图_1', + defaults: KeyCode.PageUp + }) + .register({ + id: 'viewMap_2', + name: '浏览地图_2', + defaults: KeyCode.PageDown + }) + .register({ + id: 'skillTree', + name: '技能树', + defaults: KeyCode.KeyJ + }) + .register({ + id: 'desc', + name: '百科全书', + defaults: KeyCode.KeyH + }) + //#region 功能按键 + .group('function', '功能按键') + .register({ + id: 'undo_1', + name: '回退_1', + defaults: KeyCode.KeyA + }) + .register({ + id: 'undo_2', + name: '回退_2', + defaults: KeyCode.Digit5 + }) + .register({ + id: 'redo_1', + name: '恢复_1', + defaults: KeyCode.KeyW + }) + .register({ + id: 'redo_2', + name: '恢复_2', + defaults: KeyCode.Digit6 + }) + .register({ + id: 'turn', + name: '勇士转向', + defaults: KeyCode.KeyZ + }) + .register({ + id: 'getNext_1', + name: '轻按_1', + defaults: KeyCode.Space + }) + .register({ + id: 'getNext_2', + name: '轻按_2', + defaults: KeyCode.Digit7 + }) + .register({ + id: 'mark', + name: '标记怪物', + defaults: KeyCode.KeyM + }) + .register({ + id: 'special', + name: '鼠标位置怪物属性', + defaults: KeyCode.KeyE + }) + .register({ + id: 'critical', + name: '鼠标位置怪物临界', + defaults: KeyCode.KeyC + }) + .register({ + id: 'danmaku', + name: '发送弹幕', + defaults: KeyCode.KeyA, + ctrl: true + }) + .register({ + id: 'quickEquip_1', + name: '切换/保存套装_1', + defaults: KeyCode.Digit1, + alt: true + }) + .register({ + id: 'quickEquip_2', + name: '切换/保存套装_2', + defaults: KeyCode.Digit2, + alt: true + }) + .register({ + id: 'quickEquip_3', + name: '切换/保存套装_3', + defaults: KeyCode.Digit3, + alt: true + }) + .register({ + id: 'quickEquip_4', + name: '切换/保存套装_4', + defaults: KeyCode.Digit4, + alt: true + }) + .register({ + id: 'quickEquip_5', + name: '切换/保存套装_5', + defaults: KeyCode.Digit5, + alt: true + }) + .register({ + id: 'quickEquip_6', + name: '切换/保存套装_6', + defaults: KeyCode.Digit6, + alt: true + }) + .register({ + id: 'quickEquip_7', + name: '切换/保存套装_7', + defaults: KeyCode.Digit7, + alt: true + }) + .register({ + id: 'quickEquip_8', + name: '切换/保存套装_8', + defaults: KeyCode.Digit8, + alt: true + }) + .register({ + id: 'quickEquip_9', + name: '切换/保存套装_9', + defaults: KeyCode.Digit9, + alt: true + }) + .register({ + id: 'quickEquip_0', + name: '切换/保存套装_0', + defaults: KeyCode.Digit0, + alt: true + }) + //#region 技能按键 + .group('skill', '技能按键') + .register({ + id: 'skill1', + name: '断灭之刃', + defaults: KeyCode.Digit1 + }) + .register({ + id: 'skill2', + name: '跳跃', + defaults: KeyCode.Digit2 + }) + .register({ + id: 'skill3', + name: '铸剑为盾', + defaults: KeyCode.Digit3 + }) + //#region 系统按键 + .group('system', '系统按键') + .register({ + id: 'restart', + name: '回到开始界面', + defaults: KeyCode.KeyN + }) + .register({ + id: 'comment', + name: '评论区', + defaults: KeyCode.KeyP + }) + .register({ + id: 'debug', + name: '调试模式', + defaults: KeyCode.F8 + }) + //#region 通用按键 + .group('general', '通用按键') + .register({ + id: 'exit_1', + name: '退出ui界面_1', + defaults: KeyCode.KeyX + }) + .register({ + id: 'exit_2', + name: '退出ui界面_2', + defaults: KeyCode.Escape + }) + .register({ + id: 'confirm_1', + name: '确认_1', + defaults: KeyCode.Enter + }) + .register({ + id: 'confirm_2', + name: '确认_2', + defaults: KeyCode.Space + }) + .register({ + id: 'confirm_3', + name: '确认_3', + defaults: KeyCode.KeyC + }) + //#region 开始界面 + .group('@ui_start', '开始界面') + .register({ + id: '@start_up', + name: '上移光标', + defaults: KeyCode.UpArrow + }) + .register({ + id: '@start_down', + name: '下移光标', + defaults: KeyCode.DownArrow + }) + //#region 怪物手册 + .group('@ui_book', '怪物手册') + .register({ + id: '@book_up', + name: '上移光标', + defaults: KeyCode.UpArrow + }) + .register({ + id: '@book_down', + name: '下移光标', + defaults: KeyCode.DownArrow + }) + .register({ + id: '@book_pageDown_1', + name: '下移5个怪物_1', + defaults: KeyCode.RightArrow + }) + .register({ + id: '@book_pageDown_2', + name: '下移5个怪物_2', + defaults: KeyCode.PageDown + }) + .register({ + id: '@book_pageUp_1', + name: '上移5个怪物_1', + defaults: KeyCode.LeftArrow + }) + .register({ + id: '@book_pageUp_2', + name: '上移5个怪物_2', + defaults: KeyCode.PageUp + }) + //#region 道具栏 + .group('@ui_toolbox', '道具栏') + .register({ + id: '@toolbox_right', + name: '光标右移', + defaults: KeyCode.RightArrow + }) + .register({ + id: '@toolbox_left', + name: '光标左移', + defaults: KeyCode.LeftArrow + }) + .register({ + id: '@toolbox_up', + name: '光标上移', + defaults: KeyCode.UpArrow + }) + .register({ + id: '@toolbox_down', + name: '光标下移', + defaults: KeyCode.DownArrow + }) + //#region 商店 + .group('@ui_shop', '商店') + .register({ + id: '@shop_up', + name: '上移光标', + defaults: KeyCode.UpArrow + }) + .register({ + id: '@shop_down', + name: '下移光标', + defaults: KeyCode.DownArrow + }) + .register({ + id: '@shop_add', + name: '增加购买量', + defaults: KeyCode.RightArrow + }) + .register({ + id: '@shop_min', + name: '减少购买量', + defaults: KeyCode.LeftArrow + }) + //#region 楼层传送 + .group('@ui_fly', '楼层传送') + .register({ + id: '@fly_left', + name: '左移地图', + defaults: KeyCode.LeftArrow + }) + .register({ + id: '@fly_right', + name: '右移地图', + defaults: KeyCode.RightArrow + }) + .register({ + id: '@fly_up', + name: '上移地图', + defaults: KeyCode.UpArrow + }) + .register({ + id: '@fly_down', + name: '下移地图', + defaults: KeyCode.DownArrow + }) + .register({ + id: '@fly_last', + name: '上一张地图', + defaults: KeyCode.PageDown + }) + .register({ + id: '@fly_next', + name: '下一张地图', + defaults: KeyCode.PageUp + }) + //#region 传统楼传 + .group('@ui_fly_tradition', '楼层传送-传统按键') + .register({ + id: '@fly_down_t', + name: '上一张地图', + defaults: KeyCode.DownArrow + }) + .register({ + id: '@fly_up_t', + name: '下一张地图', + defaults: KeyCode.UpArrow + }) + .register({ + id: '@fly_left_t_1', + name: '前10张地图_1', + defaults: KeyCode.LeftArrow + }) + .register({ + id: '@fly_left_t_2', + name: '前10张地图_2', + defaults: KeyCode.PageDown + }) + .register({ + id: '@fly_right_t_1', + name: '后10张地图_1', + defaults: KeyCode.RightArrow + }) + .register({ + id: '@fly_right_t_2', + name: '后10张地图_2', + defaults: KeyCode.PageUp + }) + // #region 存档界面 + .group('@ui_save', 'save') + .register({ + id: '@save_exit', + name: '退出存档界面', + defaults: KeyCode.KeyS + }) + .register({ + id: '@save_pageUp', + name: '存档向后翻页', + defaults: KeyCode.PageUp + }) + .register({ + id: '@save_pageDown', + name: '存档向前翻页', + defaults: KeyCode.PageDown + }) + .register({ + id: '@save_up', + name: '存档选择框向上', + defaults: KeyCode.UpArrow + }) + .register({ + id: '@save_down', + name: '存档选择框向下', + defaults: KeyCode.DownArrow + }) + .register({ + id: '@save_left', + name: '存档选择框向左', + defaults: KeyCode.LeftArrow + }) + .register({ + id: '@save_right', + name: '存档选择框向右', + defaults: KeyCode.RightArrow + }); +// #endregion + +gameKey.enable(); +gameKey.use(mainScope); + +//#region 按键实现 + +gameKey + .when( + () => + !core.status.lockControl && !core.isMoving() && !core.isReplaying() + ) + .realize('book', () => { + core.openBook(true); + }) + .realize('load', () => { + core.load(true); + }) + .realize('toolbox', () => { + core.openToolbox(true); + }) + .realize('equipbox', () => { + core.openEquipbox(true); + }) + .realize('fly', () => { + core.useFly(true); + }) + .realize('menu', () => { + core.openSettings(true); + }) + .realize('replay', () => { + core.ui._drawReplay(); + }) + .realize('shop', () => { + core.openQuickShop(true); + }) + .realize('viewMap', () => { + core.ui._drawViewMaps(); + }) + .realize('skillTree', () => { + core.useItem('skill1', true); + }) + .realize('desc', () => { + core.useItem('I560', true); + }) + .realize('undo', () => { + core.doSL('autoSave', 'load'); + }) + .realize('redo', () => { + core.doSL('autoSave', 'reload'); + }) + .realize('turn', () => { + core.turnHero(); + }) + .realize('getNext', () => { + core.getNextItem(); + }) + .realize('mark', () => { + const cls = hovered?.event.cls; + if (cls === 'enemys' || cls === 'enemy48') { + // const id = hovered!.event.id as EnemyIds; + // if (hasMarkedEnemy(id)) unmarkEnemy(id); + // else markEnemy(id); + } + }) + .realize('special', () => { + if (hovered) { + const { x, y } = hovered; + const enemy = core.status.thisMap.enemy.get(x, y); + if (enemy) mainUi.open('fixedDetail', { panel: 'special' }); + } + }) + .realize('critical', () => { + if (hovered) { + const { x, y } = hovered; + const enemy = core.status.thisMap.enemy.get(x, y); + if (enemy) mainUi.open('fixedDetail', { panel: 'critical' }); + } + }) + .realize('danmaku', () => { + openDanmakuPoster(); + }) + .realize('restart', () => { + core.confirmRestart(); + }) + .realize('comment', () => { + core.actions._clickGameInfo_openComments(); + }) + .realize('skill1', () => { + const HeroSkill = Mota.require('@user/data-state').HeroSkill; + if (!HeroSkill.learnedSkill(HeroSkill.Blade)) return; + if (HeroSkill.getAutoSkill()) { + tip('error', '已开启自动切换技能!'); + return; + } + core.playSound('光标移动'); + HeroSkill.toggleSkill(HeroSkill.Blade); + core.status.route.push('useSkill:Blade'); + core.updateStatusBar(); + }) + .realize('skill2', () => { + const HeroSkill = Mota.require('@user/data-state').HeroSkill; + if ( + !flags.onChase && + !core.status.floorId.startsWith('tower') && + HeroSkill.learnedSkill(HeroSkill.Jump) + ) { + Mota.require('@user/legacy-plugin-data').jumpSkill(); + core.status.route.push('useSkill:Jump'); + } else { + if (core.hasItem('pickaxe')) { + core.useItem('pickaxe'); + } + } + }) + .realize('skill3', () => { + const HeroSkill = Mota.require('@user/data-state').HeroSkill; + if (!HeroSkill.learnedSkill(HeroSkill.Shield)) return; + if (HeroSkill.getAutoSkill()) { + tip('error', '已开启自动切换技能!'); + return; + } + core.playSound('光标移动'); + HeroSkill.toggleSkill(HeroSkill.Shield); + core.status.route.push('useSkill:Shield'); + core.updateStatusBar(); + }) + .realize('debug', () => { + core.debug(); + }); + +// ----- Storage +const keyStorage = new GameStorage>( + GameStorage.fromAuthor('AncTe', 'gameKey') +); +keyStorage.data = {}; +keyStorage.read(); +gameKey.on('set', (id, key, assist) => { + keyStorage.setValue(id, { key, assist }); +}); +gameKey.fromJSON(keyStorage.toJSON()); diff --git a/packages-user/client-modules/src/action/index.ts b/packages-user/client-modules/src/action/index.ts new file mode 100644 index 0000000..0837d98 --- /dev/null +++ b/packages-user/client-modules/src/action/index.ts @@ -0,0 +1,2 @@ +export * from './move'; +export * from './hotkey'; diff --git a/packages-user/client-modules/src/action/move.ts b/packages-user/client-modules/src/action/move.ts new file mode 100644 index 0000000..7e97704 --- /dev/null +++ b/packages-user/client-modules/src/action/move.ts @@ -0,0 +1,165 @@ +import { KeyCode } from '@motajs/client-base'; +import { Hotkey, HotkeyData } from '@motajs/system-action'; +import { HeroMover, IMoveController } from '@user/data-state'; +import { Ticker } from 'mutate-animate'; +import { mainScope } from './hotkey'; + +type MoveKey = Record; +type MoveKeyConfig = Record; + +export class HeroKeyMover { + /** 当前按下的键 */ + private pressedKey: Set = new Set(); + /** 当前的移动方向 */ + private moveDir: Dir = 'down'; + /** 当前是否正在使用按键移动 */ + private moving: boolean = false; + /** 当前移动的控制器 */ + private controller?: IMoveController; + + /** 按键接续ticker */ + private ticker = new Ticker(); + + /** 当前移动实例绑定的热键 */ + hotkey: Hotkey; + /** 当前热键的移动按键信息 */ + hotkeyData: MoveKey; + /** 移动实例 */ + mover: HeroMover; + /** 移动可触发的作用域 */ + scope: symbol = mainScope; + + constructor(hotkey: Hotkey, mover: HeroMover, config?: MoveKeyConfig) { + this.hotkey = hotkey; + this.mover = mover; + hotkey.on('press', this.onPressKey); + hotkey.on('release', this.onReleaseKey); + + const data = hotkey.data; + + this.hotkeyData = { + left: data[config?.left ?? 'moveLeft'], + right: data[config?.right ?? 'moveRight'], + up: data[config?.up ?? 'moveUp'], + down: data[config?.down ?? 'moveDown'] + }; + + this.ticker.add(() => { + if (!this.moving) { + if (this.pressedKey.size > 0) { + const dir = [...this.pressedKey].at(-1); + if (!dir) return; + this.moveDir = dir; + this.tryStartMove(); + } + } + }); + } + + private onPressKey = (code: KeyCode) => { + if (core.isReplaying() || !core.isPlaying()) return; + core.waitHeroToStop(); + if (code === this.hotkeyData.left.key) this.press('left'); + else if (code === this.hotkeyData.right.key) this.press('right'); + else if (code === this.hotkeyData.up.key) this.press('up'); + else if (code === this.hotkeyData.down.key) this.press('down'); + }; + + private onReleaseKey = (code: KeyCode) => { + if (code === this.hotkeyData.left.key) this.release('left'); + else if (code === this.hotkeyData.right.key) this.release('right'); + else if (code === this.hotkeyData.up.key) this.release('up'); + else if (code === this.hotkeyData.down.key) this.release('down'); + }; + + /** + * 设置按键触发作用域 + */ + setScope(scope: symbol) { + this.scope = scope; + } + + /** + * 按下某个方向键 + * @param dir 移动方向 + */ + press(dir: Dir) { + if (this.hotkey.scope !== this.scope || core.status.lockControl) return; + this.pressedKey.add(dir); + this.moveDir = dir; + if (!this.moving) { + this.tryStartMove(); + } + } + + /** + * 松开方向键 + * @param dir 移动方向 + */ + release(dir: Dir) { + this.pressedKey.delete(dir); + if (this.pressedKey.size > 0) { + this.moveDir = [...this.pressedKey][0]; + } else { + this.endMove(); + } + } + + /** + * 尝试开始移动 + * @returns 是否成功开始移动 + */ + tryStartMove() { + if (this.moving || core.status.lockControl) return false; + + this.mover.oneStep(this.moveDir); + const controller = this.mover.startMove(false, false, false, true); + if (!controller) return false; + + this.controller = controller; + controller.onEnd.then(() => { + this.moving = false; + this.controller = void 0; + this.mover.off('stepEnd', this.onStepEnd); + }); + this.moving = true; + + this.mover.on('stepEnd', this.onStepEnd); + return true; + } + + /** + * 停止本次按键移动 + */ + endMove() { + this.controller?.stop(); + } + + private onStepEnd = () => { + const con = this.controller; + if (!con) return; + if (core.status.lockControl) { + con.stop(); + return; + } + if (!this.moving) { + con.stop(); + return; + } + + if (this.pressedKey.size > 0) { + if (con.queue.length === 0) { + con.push({ type: 'dir', value: this.moveDir }); + } + } else { + con.stop(); + } + }; + + destroy() { + this.hotkey.off('press', this.onPressKey); + this.hotkey.off('release', this.onReleaseKey); + this.mover.off('stepEnd', this.onStepEnd); + this.ticker.destroy(); + } +} diff --git a/packages-user/client-modules/src/audio/bgm.ts b/packages-user/client-modules/src/audio/bgm.ts new file mode 100644 index 0000000..f983414 --- /dev/null +++ b/packages-user/client-modules/src/audio/bgm.ts @@ -0,0 +1,268 @@ +import EventEmitter from 'eventemitter3'; +import { audioPlayer, AudioPlayer, AudioRoute, AudioStatus } from './player'; +import { guessTypeByExt, isAudioSupport } from './support'; +import { logger } from '@motajs/common'; +import { StreamLoader } from '../loader'; +import { linear, sleep, Transition } from 'mutate-animate'; +import { VolumeEffect } from './effect'; + +interface BgmVolume { + effect: VolumeEffect; + transition: Transition; +} + +interface BgmControllerEvent { + play: []; + pause: []; + resume: []; + stop: []; +} + +export class BgmController< + T extends string = BgmIds +> extends EventEmitter { + /** bgm音频名称的前缀 */ + prefix: string = 'bgms.'; + /** 每个 bgm 的音量控制器 */ + readonly gain: Map = new Map(); + + /** 正在播放的 bgm */ + playingBgm?: T; + /** 是否正在播放 */ + playing: boolean = false; + + /** 是否已经启用 */ + enabled: boolean = true; + /** 主音量控制器 */ + private readonly mainGain: VolumeEffect; + /** 是否屏蔽所有的音乐切换 */ + private blocking: boolean = false; + /** 渐变时长 */ + private transitionTime: number = 2000; + + constructor(public readonly player: AudioPlayer) { + super(); + this.mainGain = player.createVolumeEffect(); + } + + /** + * 设置音频渐变时长 + * @param time 渐变时长 + */ + setTransitionTime(time: number) { + this.transitionTime = time; + for (const [, value] of this.gain) { + value.transition.time(time); + } + } + + /** + * 屏蔽音乐切换 + */ + blockChange() { + this.blocking = true; + } + + /** + * 取消屏蔽音乐切换 + */ + unblockChange() { + this.blocking = false; + } + + /** + * 设置总音量大小 + * @param volume 音量大小 + */ + setVolume(volume: number) { + this.mainGain.setVolume(volume); + } + + /** + * 获取总音量大小 + */ + getVolume() { + return this.mainGain.getVolume(); + } + + /** + * 设置是否启用 + * @param enabled 是否启用 + */ + setEnabled(enabled: boolean) { + if (enabled) this.resume(); + else this.stop(); + this.enabled = enabled; + } + + /** + * 设置 bgm 音频名称的前缀 + */ + setPrefix(prefix: string) { + this.prefix = prefix; + } + + private getId(name: T) { + return `${this.prefix}${name}`; + } + + /** + * 根据 bgm 名称获取其 AudioRoute 实例 + * @param id 音频名称 + */ + get(id: T) { + return this.player.getRoute(this.getId(id)); + } + + /** + * 添加一个 bgm + * @param id 要添加的 bgm 的名称 + * @param url 指定 bgm 的加载地址 + */ + addBgm(id: T, url: string = `project/bgms/${id}`) { + const type = guessTypeByExt(id); + if (!type) { + logger.warn(50, id.split('.').slice(0, -1).join('.')); + return; + } + const gain = this.player.createVolumeEffect(); + if (isAudioSupport(type)) { + const source = audioPlayer.createElementSource(); + source.setSource(url); + source.setLoop(true); + const route = new AudioRoute(source, audioPlayer); + route.addEffect([gain, this.mainGain]); + audioPlayer.addRoute(this.getId(id), route); + this.setTransition(id, route, gain); + } else { + const source = audioPlayer.createStreamSource(); + const stream = new StreamLoader(url); + stream.pipe(source); + source.setLoop(true); + const route = new AudioRoute(source, audioPlayer); + route.addEffect([gain, this.mainGain]); + audioPlayer.addRoute(this.getId(id), route); + this.setTransition(id, route, gain); + } + } + + /** + * 移除一个 bgm + * @param id 要移除的 bgm 的名称 + */ + removeBgm(id: T) { + this.player.removeRoute(this.getId(id)); + const gain = this.gain.get(id); + gain?.transition.ticker.destroy(); + this.gain.delete(id); + } + + private setTransition(id: T, route: AudioRoute, gain: VolumeEffect) { + const transition = new Transition(); + transition + .time(this.transitionTime) + .mode(linear()) + .transition('volume', 0); + + const tick = () => { + gain.setVolume(transition.value.volume); + }; + + /** + * @param expect 在结束时应该是正在播放还是停止 + */ + const setTick = async (expect: AudioStatus) => { + transition.ticker.remove(tick); + transition.ticker.add(tick); + const identifier = route.stopIdentifier; + await sleep(this.transitionTime + 500); + if ( + route.status === expect && + identifier === route.stopIdentifier + ) { + transition.ticker.remove(tick); + if (route.status === AudioStatus.Playing) { + gain.setVolume(1); + } else { + gain.setVolume(0); + } + } + }; + + route.onStart(async () => { + transition.transition('volume', 1); + setTick(AudioStatus.Playing); + }); + route.onEnd(() => { + transition.transition('volume', 0); + setTick(AudioStatus.Paused); + }); + route.setEndTime(this.transitionTime); + + this.gain.set(id, { effect: gain, transition }); + } + + /** + * 播放一个 bgm + * @param id 要播放的 bgm 名称 + */ + play(id: T, when?: number) { + if (this.blocking) return; + if (id !== this.playingBgm && this.playingBgm) { + this.player.pause(this.getId(this.playingBgm)); + } + this.playingBgm = id; + if (!this.enabled) return; + this.player.play(this.getId(id), when); + this.playing = true; + this.emit('play'); + } + + /** + * 继续当前的 bgm + */ + resume() { + if (this.blocking || !this.enabled || this.playing) return; + if (this.playingBgm) { + this.player.resume(this.getId(this.playingBgm)); + } + this.playing = true; + this.emit('resume'); + } + + /** + * 暂停当前的 bgm + */ + pause() { + if (this.blocking || !this.enabled) return; + if (this.playingBgm) { + this.player.pause(this.getId(this.playingBgm)); + } + this.playing = false; + this.emit('pause'); + } + + /** + * 停止当前的 bgm + */ + stop() { + if (this.blocking || !this.enabled) return; + if (this.playingBgm) { + this.player.stop(this.getId(this.playingBgm)); + } + this.playing = false; + this.emit('stop'); + } +} + +export const bgmController = new BgmController(audioPlayer); + +export function loadAllBgm() { + const { loading } = Mota.require('@user/data-base'); + loading.once('coreInit', () => { + const data = data_a1e2fb4a_e986_4524_b0da_9b7ba7c0874d; + for (const bgm of data.main.bgms) { + bgmController.addBgm(bgm); + } + }); +} diff --git a/packages-user/client-modules/src/audio/decoder.ts b/packages-user/client-modules/src/audio/decoder.ts new file mode 100644 index 0000000..28f4be6 --- /dev/null +++ b/packages-user/client-modules/src/audio/decoder.ts @@ -0,0 +1,201 @@ +import { logger } from '@motajs/common'; +import { OggVorbisDecoderWebWorker } from '@wasm-audio-decoders/ogg-vorbis'; +import { OggOpusDecoderWebWorker } from 'ogg-opus-decoder'; +import { AudioType, isAudioSupport } from './support'; +import type { AudioPlayer } from './player'; + +const fileSignatures: [AudioType, number[]][] = [ + [AudioType.Mp3, [0x49, 0x44, 0x33]], + [AudioType.Ogg, [0x4f, 0x67, 0x67, 0x53]], + [AudioType.Wav, [0x52, 0x49, 0x46, 0x46]], + [AudioType.Flac, [0x66, 0x4c, 0x61, 0x43]], + [AudioType.Aac, [0xff, 0xf1]], + [AudioType.Aac, [0xff, 0xf9]] +]; +const oggHeaders: [AudioType, number[]][] = [ + [AudioType.Opus, [0x4f, 0x70, 0x75, 0x73, 0x48, 0x65, 0x61, 0x64]] +]; + +export function checkAudioType(data: Uint8Array) { + let audioType: AudioType | '' = ''; + // 检查头文件获取音频类型,仅检查前256个字节 + const toCheck = data.slice(0, 256); + for (const [type, value] of fileSignatures) { + if (value.every((v, i) => toCheck[i] === v)) { + audioType = type; + break; + } + } + if (audioType === AudioType.Ogg) { + // 如果是ogg的话,进一步判断是不是opus + for (const [key, value] of oggHeaders) { + const has = toCheck.some((_, i) => { + return value.every((v, ii) => toCheck[i + ii] === v); + }); + if (has) { + audioType = key; + break; + } + } + } + return audioType; +} + +export interface IAudioDecodeError { + /** 错误信息 */ + message: string; +} + +export interface IAudioDecodeData { + /** 每个声道的音频信息 */ + channelData: Float32Array[]; + /** 已经被解码的 PCM 采样数 */ + samplesDecoded: number; + /** 音频采样率 */ + sampleRate: number; + /** 解码错误信息 */ + errors: IAudioDecodeError[]; +} + +export abstract class AudioDecoder { + static readonly decoderMap: Map AudioDecoder> = + new Map(); + + /** + * 注册一个解码器 + * @param type 要注册的解码器允许解码的类型 + * @param decoder 解码器对象 + */ + static registerDecoder(type: AudioType, decoder: new () => AudioDecoder) { + if (this.decoderMap.has(type)) { + logger.warn(47, type); + return; + } + this.decoderMap.set(type, decoder); + } + + /** + * 解码音频数据 + * @param data 音频文件数据 + * @param player AudioPlayer实例 + */ + static async decodeAudioData(data: Uint8Array, player: AudioPlayer) { + // 检查头文件获取音频类型,仅检查前256个字节 + const toCheck = data.slice(0, 256); + const type = checkAudioType(data); + if (type === '') { + logger.error( + 25, + [...toCheck] + .map(v => v.toString(16).padStart(2, '0')) + .join(' ') + .toUpperCase() + ); + return null; + } + if (isAudioSupport(type)) { + if (data.buffer instanceof ArrayBuffer) { + return player.ac.decodeAudioData(data.buffer); + } else { + return null; + } + } else { + const Decoder = this.decoderMap.get(type); + if (!Decoder) { + return null; + } else { + const decoder = new Decoder(); + await decoder.create(); + const decodedData = await decoder.decodeAll(data); + if (!decodedData) return null; + const buffer = player.ac.createBuffer( + decodedData.channelData.length, + decodedData.channelData[0].length, + decodedData.sampleRate + ); + decodedData.channelData.forEach((v, i) => { + buffer.copyToChannel(v, i); + }); + decoder.destroy(); + return buffer; + } + } + } + + /** + * 创建音频解码器 + */ + abstract create(): Promise; + + /** + * 摧毁这个解码器 + */ + abstract destroy(): void; + + /** + * 解码流数据 + * @param data 流数据 + */ + abstract decode(data: Uint8Array): Promise; + + /** + * 解码整个文件 + * @param data 文件数据 + */ + abstract decodeAll(data: Uint8Array): Promise; + + /** + * 当音频解码完成后,会调用此函数,需要返回之前还未解析或未返回的音频数据。调用后,该解码器将不会被再次使用 + */ + abstract flush(): Promise; +} + +export class VorbisDecoder extends AudioDecoder { + decoder?: OggVorbisDecoderWebWorker; + + async create(): Promise { + this.decoder = new OggVorbisDecoderWebWorker(); + await this.decoder.ready; + } + + destroy(): void { + this.decoder?.free(); + } + + async decode(data: Uint8Array): Promise { + return this.decoder?.decode(data); + } + + async decodeAll(data: Uint8Array): Promise { + return this.decoder?.decodeFile(data); + } + + async flush(): Promise { + return this.decoder?.flush(); + } +} + +export class OpusDecoder extends AudioDecoder { + decoder?: OggOpusDecoderWebWorker; + + async create(): Promise { + this.decoder = new OggOpusDecoderWebWorker(); + await this.decoder.ready; + } + + destroy(): void { + this.decoder?.free(); + } + + async decode(data: Uint8Array): Promise { + return this.decoder?.decode(data); + } + + async decodeAll(data: Uint8Array): Promise { + return this.decoder?.decodeFile(data); + } + + async flush(): Promise { + return await this.decoder?.flush(); + } +} diff --git a/packages-user/client-modules/src/audio/effect.ts b/packages-user/client-modules/src/audio/effect.ts new file mode 100644 index 0000000..1471058 --- /dev/null +++ b/packages-user/client-modules/src/audio/effect.ts @@ -0,0 +1,288 @@ +import { isNil } from 'lodash-es'; +import { sleep } from 'mutate-animate'; + +export interface IAudioInput { + /** 输入节点 */ + input: AudioNode; +} + +export interface IAudioOutput { + /** 输出节点 */ + output: AudioNode; +} + +export abstract class AudioEffect implements IAudioInput, IAudioOutput { + /** 输出节点 */ + abstract output: AudioNode; + /** 输入节点 */ + abstract input: AudioNode; + + constructor(public readonly ac: AudioContext) {} + + /** + * 当音频播放结束时触发,可以用于节点结束后处理 + */ + abstract end(): void; + + /** + * 当音频开始播放时触发,可以用于节点初始化 + */ + abstract start(): void; + + /** + * 连接至其他效果器 + * @param target 目标输入 + * @param output 当前效果器输出通道 + * @param input 目标效果器的输入通道 + */ + connect(target: IAudioInput, output?: number, input?: number) { + this.output.connect(target.input, output, input); + } + + /** + * 与其他效果器取消连接 + * @param target 目标输入 + * @param output 当前效果器输出通道 + * @param input 目标效果器的输入通道 + */ + disconnect(target?: IAudioInput, output?: number, input?: number) { + if (!target) { + if (!isNil(output)) { + this.output.disconnect(output); + } else { + this.output.disconnect(); + } + } else { + if (!isNil(output)) { + if (!isNil(input)) { + this.output.disconnect(target.input, output, input); + } else { + this.output.disconnect(target.input, output); + } + } else { + this.output.disconnect(target.input); + } + } + } +} + +export class StereoEffect extends AudioEffect { + output: PannerNode; + input: PannerNode; + + constructor(ac: AudioContext) { + super(ac); + const panner = ac.createPanner(); + this.input = panner; + this.output = panner; + } + + /** + * 设置音频朝向,x正方形水平向右,y正方形垂直于地面向上,z正方向垂直屏幕远离用户 + * @param x 朝向x坐标 + * @param y 朝向y坐标 + * @param z 朝向z坐标 + */ + setOrientation(x: number, y: number, z: number) { + this.output.orientationX.value = x; + this.output.orientationY.value = y; + this.output.orientationZ.value = z; + } + + /** + * 设置音频位置,x正方形水平向右,y正方形垂直于地面向上,z正方向垂直屏幕远离用户 + * @param x 位置x坐标 + * @param y 位置y坐标 + * @param z 位置z坐标 + */ + setPosition(x: number, y: number, z: number) { + this.output.positionX.value = x; + this.output.positionY.value = y; + this.output.positionZ.value = z; + } + + end(): void {} + + start(): void {} +} + +export class VolumeEffect extends AudioEffect { + output: GainNode; + input: GainNode; + + constructor(ac: AudioContext) { + super(ac); + const gain = ac.createGain(); + this.input = gain; + this.output = gain; + } + + /** + * 设置音量大小 + * @param volume 音量大小 + */ + setVolume(volume: number) { + this.output.gain.value = volume; + } + + /** + * 获取音量大小 + */ + getVolume(): number { + return this.output.gain.value; + } + + end(): void {} + + start(): void {} +} + +export class ChannelVolumeEffect extends AudioEffect { + output: ChannelMergerNode; + input: ChannelSplitterNode; + + /** 所有的音量控制节点 */ + private readonly gain: GainNode[] = []; + + constructor(ac: AudioContext) { + super(ac); + const splitter = ac.createChannelSplitter(); + const merger = ac.createChannelMerger(); + this.output = merger; + this.input = splitter; + for (let i = 0; i < 6; i++) { + const gain = ac.createGain(); + splitter.connect(gain, i); + gain.connect(merger, 0, i); + this.gain.push(gain); + } + } + + /** + * 设置某个声道的音量大小 + * @param channel 要设置的声道,可填0-5 + * @param volume 这个声道的音量大小 + */ + setVolume(channel: number, volume: number) { + if (!this.gain[channel]) return; + this.gain[channel].gain.value = volume; + } + + /** + * 获取某个声道的音量大小,可填0-5 + * @param channel 要获取的声道 + */ + getVolume(channel: number): number { + if (!this.gain[channel]) return 0; + return this.gain[channel].gain.value; + } + + end(): void {} + + start(): void {} +} + +export class DelayEffect extends AudioEffect { + output: DelayNode; + input: DelayNode; + + constructor(ac: AudioContext) { + super(ac); + const delay = ac.createDelay(); + this.input = delay; + this.output = delay; + } + + /** + * 设置延迟时长 + * @param delay 延迟时长,单位秒 + */ + setDelay(delay: number) { + this.output.delayTime.value = delay; + } + + /** + * 获取延迟时长 + */ + getDelay() { + return this.output.delayTime.value; + } + + end(): void {} + + start(): void {} +} + +export class EchoEffect extends AudioEffect { + output: GainNode; + input: GainNode; + + /** 延迟节点 */ + private readonly delay: DelayNode; + /** 反馈增益节点 */ + private readonly gainNode: GainNode; + /** 当前增益 */ + private gain: number = 0.5; + /** 是否正在播放 */ + private playing: boolean = false; + + constructor(ac: AudioContext) { + super(ac); + const delay = ac.createDelay(); + const gain = ac.createGain(); + gain.gain.value = 0.5; + delay.delayTime.value = 0.05; + delay.connect(gain); + gain.connect(delay); + this.delay = delay; + this.gainNode = gain; + this.input = gain; + this.output = gain; + } + + /** + * 设置回声反馈增益大小 + * @param gain 增益大小,范围 0-1,大于等于1的视为0.5,小于0的视为0 + */ + setFeedbackGain(gain: number) { + const resolved = gain >= 1 ? 0.5 : gain < 0 ? 0 : gain; + this.gain = resolved; + if (this.playing) this.gainNode.gain.value = resolved; + } + + /** + * 设置回声间隔时长 + * @param delay 回声时长,范围 0.01-Infinity,小于0.01的视为0.01 + */ + setEchoDelay(delay: number) { + const resolved = delay < 0.01 ? 0.01 : delay; + this.delay.delayTime.value = resolved; + } + + /** + * 获取反馈节点增益 + */ + getFeedbackGain() { + return this.gain; + } + + /** + * 获取回声间隔时长 + */ + getEchoDelay() { + return this.delay.delayTime.value; + } + + end(): void { + this.playing = false; + const echoTime = Math.ceil(Math.log(0.001) / Math.log(this.gain)) + 10; + sleep(this.delay.delayTime.value * echoTime).then(() => { + if (!this.playing) this.gainNode.gain.value = 0; + }); + } + + start(): void { + this.playing = true; + this.gainNode.gain.value = this.gain; + } +} diff --git a/packages-user/client-modules/src/audio/index.ts b/packages-user/client-modules/src/audio/index.ts new file mode 100644 index 0000000..c09a229 --- /dev/null +++ b/packages-user/client-modules/src/audio/index.ts @@ -0,0 +1,18 @@ +import { loadAllBgm } from './bgm'; +import { OpusDecoder, VorbisDecoder } from './decoder'; +import { AudioType } from './support'; +import { AudioDecoder } from './decoder'; + +export function createAudio() { + loadAllBgm(); + AudioDecoder.registerDecoder(AudioType.Ogg, VorbisDecoder); + AudioDecoder.registerDecoder(AudioType.Opus, OpusDecoder); +} + +export * from './support'; +export * from './effect'; +export * from './player'; +export * from './source'; +export * from './bgm'; +export * from './decoder'; +export * from './sound'; diff --git a/packages-user/client-modules/src/audio/player.ts b/packages-user/client-modules/src/audio/player.ts new file mode 100644 index 0000000..fc7100f --- /dev/null +++ b/packages-user/client-modules/src/audio/player.ts @@ -0,0 +1,605 @@ +import EventEmitter from 'eventemitter3'; +import { + AudioBufferSource, + AudioElementSource, + AudioSource, + AudioStreamSource +} from './source'; +import { + AudioEffect, + ChannelVolumeEffect, + DelayEffect, + EchoEffect, + IAudioOutput, + StereoEffect, + VolumeEffect +} from './effect'; +import { isNil } from 'lodash-es'; +import { logger } from '@motajs/common'; +import { sleep } from 'mutate-animate'; +import { AudioDecoder } from './decoder'; + +interface AudioPlayerEvent {} + +export class AudioPlayer extends EventEmitter { + /** 音频播放上下文 */ + readonly ac: AudioContext; + + /** 所有的音频播放路由 */ + readonly audioRoutes: Map = new Map(); + /** 音量节点 */ + readonly gain: GainNode; + + constructor() { + super(); + this.ac = new AudioContext(); + this.gain = this.ac.createGain(); + this.gain.connect(this.ac.destination); + } + + /** + * 解码音频数据 + * @param data 音频数据 + */ + decodeAudioData(data: Uint8Array) { + return AudioDecoder.decodeAudioData(data, this); + } + + /** + * 设置音量 + * @param volume 音量 + */ + setVolume(volume: number) { + this.gain.gain.value = volume; + } + + /** + * 获取音量 + */ + getVolume() { + return this.gain.gain.value; + } + + /** + * 创建一个音频源 + * @param Source 音频源类 + */ + createSource( + Source: new (ac: AudioContext) => T + ): T { + return new Source(this.ac); + } + + /** + * 创建一个兼容流式音频源,可以与流式加载相结合,主要用于处理 opus ogg 不兼容的情况 + */ + createStreamSource() { + return new AudioStreamSource(this.ac); + } + + /** + * 创建一个通过 audio 元素播放的音频源 + */ + createElementSource() { + return new AudioElementSource(this.ac); + } + + /** + * 创建一个通过 AudioBuffer 播放的音频源 + */ + createBufferSource() { + return new AudioBufferSource(this.ac); + } + + /** + * 获取音频目的地 + */ + getDestination() { + return this.gain; + } + + /** + * 创建一个音频效果器 + * @param Effect 效果器类 + */ + createEffect( + Effect: new (ac: AudioContext) => T + ): T { + return new Effect(this.ac); + } + + /** + * 创建一个修改音量的效果器 + * ```txt + * |----------| + * Input ----> | GainNode | ----> Output + * |----------| + * ``` + */ + createVolumeEffect() { + return new VolumeEffect(this.ac); + } + + /** + * 创建一个立体声效果器 + * ```txt + * |------------| + * Input ----> | PannerNode | ----> Output + * |------------| + * ``` + */ + createStereoEffect() { + return new StereoEffect(this.ac); + } + + /** + * 创建一个修改单个声道音量的效果器 + * ```txt + * |----------| + * -> | GainNode | \ + * |--------------| / |----------| -> |------------| + * Input ----> | SplitterNode | ...... | MergerNode | ----> Output + * |--------------| \ |----------| -> |------------| + * -> | GainNode | / + * |----------| + * ``` + */ + createChannelVolumeEffect() { + return new ChannelVolumeEffect(this.ac); + } + + /** + * 创建一个延迟效果器 + * ```txt + * |-----------| + * Input ----> | DelayNode | ----> Output + * |-----------| + * ``` + */ + createDelayEffect() { + return new DelayEffect(this.ac); + } + + /** + * 创建一个回声效果器 + * ```txt + * |----------| + * Input ----> | GainNode | ----> Output + * ^ |----------| | + * | | + * | |------------| ↓ + * |-- | Delay Node | <-- + * |------------| + * ``` + */ + createEchoEffect() { + return new EchoEffect(this.ac); + } + + /** + * 创建一个音频播放路由 + * @param source 音频源 + */ + createRoute(source: AudioSource) { + return new AudioRoute(source, this); + } + + /** + * 添加一个音频播放路由,可以直接被播放 + * @param id 这个音频播放路由的名称 + * @param route 音频播放路由对象 + */ + addRoute(id: string, route: AudioRoute) { + if (this.audioRoutes.has(id)) { + logger.warn(45, id); + } + this.audioRoutes.set(id, route); + } + + /** + * 根据名称获取音频播放路由对象 + * @param id 音频播放路由的名称 + */ + getRoute(id: string) { + return this.audioRoutes.get(id); + } + + /** + * 移除一个音频播放路由 + * @param id 要移除的播放路由的名称 + */ + removeRoute(id: string) { + const route = this.audioRoutes.get(id); + if (route) { + route.destroy(); + } + this.audioRoutes.delete(id); + } + + /** + * 播放音频 + * @param id 音频名称 + * @param when 从音频的哪个位置开始播放,单位秒 + */ + play(id: string, when: number = 0) { + const route = this.getRoute(id); + if (!route) { + logger.warn(53, 'play', id); + return; + } + route.play(when); + } + + /** + * 暂停音频播放 + * @param id 音频名称 + * @returns 当音乐真正停止时兑现 + */ + pause(id: string) { + const route = this.getRoute(id); + if (!route) { + logger.warn(53, 'pause', id); + return; + } + return route.pause(); + } + + /** + * 停止音频播放 + * @param id 音频名称 + * @returns 当音乐真正停止时兑现 + */ + stop(id: string) { + const route = this.getRoute(id); + if (!route) { + logger.warn(53, 'stop', id); + return; + } + return route.stop(); + } + + /** + * 继续音频播放 + * @param id 音频名称 + */ + resume(id: string) { + const route = this.getRoute(id); + if (!route) { + logger.warn(53, 'resume', id); + return; + } + route.resume(); + } + + /** + * 设置听者位置,x正方向水平向右,y正方向垂直于地面向上,z正方向垂直屏幕远离用户 + * @param x 位置x坐标 + * @param y 位置y坐标 + * @param z 位置z坐标 + */ + setListenerPosition(x: number, y: number, z: number) { + const listener = this.ac.listener; + listener.positionX.value = x; + listener.positionY.value = y; + listener.positionZ.value = z; + } + + /** + * 设置听者朝向,x正方向水平向右,y正方向垂直于地面向上,z正方向垂直屏幕远离用户 + * @param x 朝向x坐标 + * @param y 朝向y坐标 + * @param z 朝向z坐标 + */ + setListenerOrientation(x: number, y: number, z: number) { + const listener = this.ac.listener; + listener.forwardX.value = x; + listener.forwardY.value = y; + listener.forwardZ.value = z; + } + + /** + * 设置听者头顶朝向,x正方向水平向右,y正方向垂直于地面向上,z正方向垂直屏幕远离用户 + * @param x 头顶朝向x坐标 + * @param y 头顶朝向y坐标 + * @param z 头顶朝向z坐标 + */ + setListenerUp(x: number, y: number, z: number) { + const listener = this.ac.listener; + listener.upX.value = x; + listener.upY.value = y; + listener.upZ.value = z; + } +} + +export const enum AudioStatus { + Playing, + Pausing, + Paused, + Stoping, + Stoped +} + +type AudioStartHook = (route: AudioRoute) => void; +type AudioEndHook = (time: number, route: AudioRoute) => void; + +interface AudioRouteEvent { + updateEffect: []; + play: []; + stop: []; + pause: []; + resume: []; +} + +export class AudioRoute + extends EventEmitter + implements IAudioOutput +{ + output: AudioNode; + + /** 效果器路由图 */ + readonly effectRoute: AudioEffect[] = []; + + /** 结束时长,当音频暂停或停止时,会经过这么长时间之后才真正终止播放,期间可以做音频淡入淡出等效果 */ + endTime: number = 0; + + /** 当前播放状态 */ + status: AudioStatus = AudioStatus.Stoped; + /** 暂停时刻 */ + private pauseTime: number = 0; + /** 暂停时播放了多长时间 */ + private pauseCurrentTime: number = 0; + + /** 音频时长,单位秒 */ + get duration() { + return this.source.duration; + } + /** 当前播放了多长时间,单位秒 */ + get currentTime() { + if (this.status === AudioStatus.Paused) { + return this.pauseCurrentTime; + } else { + return this.source.currentTime; + } + } + set currentTime(time: number) { + this.source.stop(); + this.source.play(time); + } + + private shouldStop: boolean = false; + /** + * 每次暂停或停止时自增,用于判断当前正在处理的情况。 + * 假如暂停后很快播放,然后很快暂停,那么需要根据这个来判断实际是否应该执行暂停后操作 + */ + stopIdentifier: number = 0; + + private audioStartHook?: AudioStartHook; + private audioEndHook?: AudioEndHook; + + constructor( + public readonly source: AudioSource, + public readonly player: AudioPlayer + ) { + super(); + this.output = source.output; + source.on('end', () => { + if (this.status === AudioStatus.Playing) { + this.status = AudioStatus.Stoped; + } + }); + source.on('play', () => { + if (this.status !== AudioStatus.Playing) { + this.status = AudioStatus.Playing; + } + }); + } + + /** + * 设置结束时间,暂停或停止时,会经过这么长时间才终止音频的播放,这期间可以做一下音频淡出的效果。 + * @param time 暂停或停止时,经过多长时间之后才会结束音频的播放 + */ + setEndTime(time: number) { + this.endTime = time; + } + + /** + * 当音频播放时执行的函数,可以用于音频淡入效果 + * @param fn 音频开始播放时执行的函数 + */ + onStart(fn?: AudioStartHook) { + this.audioStartHook = fn; + } + + /** + * 当音频暂停或停止时执行的函数,可以用于音频淡出效果 + * @param fn 音频在暂停或停止时执行的函数,不填时表示取消这个钩子。 + * 包含两个参数,第一个参数是结束时长,第二个参数是当前音频播放路由对象 + */ + onEnd(fn?: AudioEndHook) { + this.audioEndHook = fn; + } + + /** + * 开始播放这个音频 + * @param when 从音频的什么时候开始播放,单位秒 + */ + async play(when: number = 0) { + if (this.status === AudioStatus.Playing) return; + this.link(); + await this.player.ac.resume(); + if (this.effectRoute.length > 0) { + const first = this.effectRoute[0]; + this.source.connect(first); + const last = this.effectRoute.at(-1)!; + last.connect({ input: this.player.getDestination() }); + } else { + this.source.connect({ input: this.player.getDestination() }); + } + this.source.play(when); + this.status = AudioStatus.Playing; + this.pauseTime = 0; + this.audioStartHook?.(this); + this.startAllEffect(); + this.emit('play'); + } + + /** + * 暂停音频播放 + */ + async pause() { + if (this.status !== AudioStatus.Playing) return; + this.status = AudioStatus.Pausing; + this.stopIdentifier++; + const identifier = this.stopIdentifier; + if (this.audioEndHook) { + this.audioEndHook(this.endTime, this); + await sleep(this.endTime); + } + if ( + this.status !== AudioStatus.Pausing || + this.stopIdentifier !== identifier + ) { + return; + } + this.pauseCurrentTime = this.source.currentTime; + const time = this.source.stop(); + this.pauseTime = time; + if (this.shouldStop) { + this.status = AudioStatus.Stoped; + this.endAllEffect(); + this.emit('stop'); + this.shouldStop = false; + } else { + this.status = AudioStatus.Paused; + this.endAllEffect(); + this.emit('pause'); + } + } + + /** + * 继续音频播放 + */ + resume() { + if (this.status === AudioStatus.Playing) return; + if ( + this.status === AudioStatus.Pausing || + this.status === AudioStatus.Stoping + ) { + this.audioStartHook?.(this); + this.emit('resume'); + return; + } + if (this.status === AudioStatus.Paused) { + this.play(this.pauseTime); + } else { + this.play(0); + } + this.status = AudioStatus.Playing; + this.pauseTime = 0; + this.audioStartHook?.(this); + this.startAllEffect(); + this.emit('resume'); + } + + /** + * 停止音频播放 + */ + async stop() { + if (this.status !== AudioStatus.Playing) { + if (this.status === AudioStatus.Pausing) { + this.shouldStop = true; + } + return; + } + this.status = AudioStatus.Stoping; + this.stopIdentifier++; + const identifier = this.stopIdentifier; + if (this.audioEndHook) { + this.audioEndHook(this.endTime, this); + await sleep(this.endTime); + } + if ( + this.status !== AudioStatus.Stoping || + this.stopIdentifier !== identifier + ) { + return; + } + this.source.stop(); + this.status = AudioStatus.Stoped; + this.pauseTime = 0; + this.endAllEffect(); + this.emit('stop'); + } + + /** + * 添加效果器 + * @param effect 要添加的效果,可以是数组,表示一次添加多个 + * @param index 从哪个位置开始添加,如果大于数组长度,那么加到末尾,如果小于0,那么将会从后面往前数。默认添加到末尾 + */ + addEffect(effect: AudioEffect | AudioEffect[], index?: number) { + if (isNil(index)) { + if (effect instanceof Array) { + this.effectRoute.push(...effect); + } else { + this.effectRoute.push(effect); + } + } else { + if (effect instanceof Array) { + this.effectRoute.splice(index, 0, ...effect); + } else { + this.effectRoute.splice(index, 0, effect); + } + } + this.setOutput(); + if (this.source.playing) this.link(); + this.emit('updateEffect'); + } + + /** + * 移除一个效果器 + * @param effect 要移除的效果 + */ + removeEffect(effect: AudioEffect) { + const index = this.effectRoute.indexOf(effect); + if (index === -1) return; + this.effectRoute.splice(index, 1); + effect.disconnect(); + this.setOutput(); + if (this.source.playing) this.link(); + this.emit('updateEffect'); + } + + destroy() { + this.effectRoute.forEach(v => v.disconnect()); + } + + private setOutput() { + const effect = this.effectRoute.at(-1); + if (!effect) this.output = this.source.output; + else this.output = effect.output; + } + + /** + * 连接音频路由图 + */ + private link() { + this.effectRoute.forEach(v => v.disconnect()); + this.effectRoute.forEach((v, i) => { + const next = this.effectRoute[i + 1]; + if (next) { + v.connect(next); + } + }); + } + + private startAllEffect() { + this.effectRoute.forEach(v => v.start()); + } + + private endAllEffect() { + this.effectRoute.forEach(v => v.end()); + } +} + +export const audioPlayer = new AudioPlayer(); +// window.audioPlayer = audioPlayer; diff --git a/packages-user/client-modules/src/audio/sound.ts b/packages-user/client-modules/src/audio/sound.ts new file mode 100644 index 0000000..0033554 --- /dev/null +++ b/packages-user/client-modules/src/audio/sound.ts @@ -0,0 +1,135 @@ +import EventEmitter from 'eventemitter3'; +import { audioPlayer, AudioPlayer } from './player'; +import { logger } from '@motajs/common'; +import { VolumeEffect } from './effect'; + +type LocationArray = [number, number, number]; + +interface SoundPlayerEvent {} + +export class SoundPlayer< + T extends string = SoundIds +> extends EventEmitter { + /** 每个音效的唯一标识符 */ + private num: number = 0; + + /** 每个音效的数据 */ + readonly buffer: Map = new Map(); + /** 所有正在播放的音乐 */ + readonly playing: Set = new Set(); + /** 音量节点 */ + readonly gain: VolumeEffect; + + /** 是否已经启用 */ + enabled: boolean = true; + + constructor(public readonly player: AudioPlayer) { + super(); + this.gain = player.createVolumeEffect(); + } + + /** + * 设置是否启用音效 + * @param enabled 是否启用音效 + */ + setEnabled(enabled: boolean) { + if (!enabled) this.stopAllSounds(); + this.enabled = enabled; + } + + /** + * 设置音量大小 + * @param volume 音量大小 + */ + setVolume(volume: number) { + this.gain.setVolume(volume); + } + + /** + * 获取音量大小 + */ + getVolume() { + return this.gain.getVolume(); + } + + /** + * 添加一个音效 + * @param id 音效名称 + * @param data 音效的Uint8Array数据 + */ + async add(id: T, data: Uint8Array) { + const buffer = await this.player.decodeAudioData(data); + if (!buffer) { + logger.warn(51, id); + return; + } + this.buffer.set(id, buffer); + } + + /** + * 播放一个音效 + * @param id 音效名称 + * @param position 音频位置,[0, 0, 0]表示正中心,x轴指向水平向右,y轴指向水平向上,z轴指向竖直向上 + * @param orientation 音频朝向,[0, 1, 0]表示朝向前方 + */ + play( + id: T, + position: LocationArray = [0, 0, 0], + orientation: LocationArray = [1, 0, 0] + ) { + if (!this.enabled) return -1; + const buffer = this.buffer.get(id); + if (!buffer) { + logger.warn(52, id); + return -1; + } + const soundNum = this.num++; + const source = this.player.createBufferSource(); + source.setBuffer(buffer); + const route = this.player.createRoute(source); + const stereo = this.player.createStereoEffect(); + stereo.setPosition(position[0], position[1], position[2]); + stereo.setOrientation(orientation[0], orientation[1], orientation[2]); + route.addEffect([stereo, this.gain]); + this.player.addRoute(`sounds.${soundNum}`, route); + route.play(); + // 清理垃圾 + source.output.addEventListener('ended', () => { + this.playing.delete(soundNum); + this.player.removeRoute(`sounds.${soundNum}`); + }); + this.playing.add(soundNum); + return soundNum; + } + + /** + * 停止一个音效 + * @param num 音效的唯一 id + */ + stop(num: number) { + const id = `sounds.${num}`; + const route = this.player.getRoute(id); + if (route) { + route.stop(); + this.player.removeRoute(id); + this.playing.delete(num); + } + } + + /** + * 停止播放所有音效 + */ + stopAllSounds() { + this.playing.forEach(v => { + const id = `sounds.${v}`; + const route = this.player.getRoute(id); + if (route) { + route.stop(); + this.player.removeRoute(id); + } + }); + this.playing.clear(); + } +} + +export const soundPlayer = new SoundPlayer(audioPlayer); diff --git a/packages-user/client-modules/src/audio/source.ts b/packages-user/client-modules/src/audio/source.ts new file mode 100644 index 0000000..5040219 --- /dev/null +++ b/packages-user/client-modules/src/audio/source.ts @@ -0,0 +1,562 @@ +import EventEmitter from 'eventemitter3'; +import { IStreamController, IStreamReader } from '../loader'; +import { IAudioInput, IAudioOutput } from './effect'; +import { logger } from '@motajs/common'; +import { AudioType } from './support'; +import CodecParser, { CodecFrame, MimeType, OggPage } from 'codec-parser'; +import { isNil } from 'lodash-es'; +import { IAudioDecodeData, AudioDecoder, checkAudioType } from './decoder'; + +interface AudioSourceEvent { + play: []; + end: []; +} + +export abstract class AudioSource + extends EventEmitter + implements IAudioOutput +{ + /** 音频源的输出节点 */ + abstract readonly output: AudioNode; + + /** 是否正在播放 */ + playing: boolean = false; + + /** 获取音频时长 */ + abstract get duration(): number; + /** 获取当前音频播放了多长时间 */ + abstract get currentTime(): number; + + constructor(public readonly ac: AudioContext) { + super(); + } + + /** + * 开始播放这个音频源 + */ + abstract play(when?: number): void; + + /** + * 停止播放这个音频源 + * @returns 音频暂停的时刻 + */ + abstract stop(): number; + + /** + * 连接到音频路由图上,每次调用播放的时候都会执行一次 + * @param target 连接至的目标 + */ + abstract connect(target: IAudioInput): void; + + /** + * 设置是否循环播放 + * @param loop 是否循环 + */ + abstract setLoop(loop: boolean): void; +} + +const mimeTypeMap: Record = { + [AudioType.Aac]: 'audio/aac', + [AudioType.Flac]: 'audio/flac', + [AudioType.Mp3]: 'audio/mpeg', + [AudioType.Ogg]: 'application/ogg', + [AudioType.Opus]: 'application/ogg', + [AudioType.Wav]: 'application/ogg' +}; + +function isOggPage(data: any): data is OggPage { + return !isNil(data.isFirstPage); +} + +export class AudioStreamSource extends AudioSource implements IStreamReader { + output: AudioBufferSourceNode; + + /** 音频数据 */ + buffer?: AudioBuffer; + + /** 是否已经完全加载完毕 */ + loaded: boolean = false; + /** 已经缓冲了多长时间,如果缓冲完那么跟歌曲时长一致 */ + buffered: number = 0; + /** 已经缓冲的采样点数量 */ + bufferedSamples: number = 0; + /** 歌曲时长,加载完毕之前保持为 0 */ + duration: number = 0; + /** 当前已经播放了多长时间 */ + get currentTime(): number { + return this.ac.currentTime - this.lastStartTime + this.lastStartWhen; + } + /** 在流传输阶段,至少缓冲多长时间的音频之后才开始播放,单位秒 */ + bufferPlayDuration: number = 1; + /** 音频的采样率,未成功解析出之前保持为 0 */ + sampleRate: number = 0; + + private controller?: IStreamController; + private loop: boolean = false; + + private target?: IAudioInput; + + /** 上一次播放是从何时开始的 */ + private lastStartWhen: number = 0; + /** 开始播放时刻 */ + private lastStartTime: number = 0; + /** 上一次播放的缓存长度 */ + private lastBufferSamples: number = 0; + + /** 是否已经获取到头文件 */ + private headerRecieved: boolean = false; + /** 音频类型 */ + private audioType: AudioType | '' = ''; + /** 音频解码器 */ + private decoder?: AudioDecoder; + /** 音频解析器 */ + private parser?: CodecParser; + /** 每多长时间组成一个缓存 Float32Array */ + private bufferChunkSize: number = 10; + /** 缓存音频数据,每 bufferChunkSize 秒钟组成一个 Float32Array,用于流式解码 */ + private audioData: Float32Array[][] = []; + + private errored: boolean = false; + + constructor(context: AudioContext) { + super(context); + this.output = context.createBufferSource(); + } + + /** + * 设置每个缓存数据的大小,默认为10秒钟一个缓存数据 + * @param size 每个缓存数据的时长,单位秒 + */ + setChunkSize(size: number) { + if (this.controller?.loading || this.loaded) return; + this.bufferChunkSize = size; + } + + piped(controller: IStreamController): void { + this.controller = controller; + } + + async pump(data: Uint8Array | undefined, done: boolean): Promise { + if (!data || this.errored) return; + if (!this.headerRecieved) { + // 检查头文件获取音频类型,仅检查前256个字节 + const toCheck = data.slice(0, 256); + this.audioType = checkAudioType(data); + if (!this.audioType) { + logger.error( + 25, + [...toCheck] + .map(v => v.toString(16).padStart(2, '0')) + .join(' ') + .toUpperCase() + ); + return; + } + // 创建解码器 + const Decoder = AudioDecoder.decoderMap.get(this.audioType); + if (!Decoder) { + this.errored = true; + logger.error(24, this.audioType); + return Promise.reject( + `Cannot decode stream source type of '${this.audioType}', since there is no registered decoder for that type.` + ); + } + this.decoder = new Decoder(); + // 创建数据解析器 + const mime = mimeTypeMap[this.audioType]; + const parser = new CodecParser(mime); + this.parser = parser; + await this.decoder.create(); + this.headerRecieved = true; + } + + const decoder = this.decoder; + const parser = this.parser; + if (!decoder || !parser) { + this.errored = true; + return Promise.reject( + 'No parser or decoder attached in this AudioStreamSource' + ); + } + + await this.decodeData(data, decoder, parser); + if (done) await this.decodeFlushData(decoder, parser); + this.checkBufferedPlay(); + } + + /** + * 检查采样率,如果还未解析出采样率,那么将设置采样率,如果当前采样率与之前不同,那么发出警告 + */ + private checkSampleRate(info: (OggPage | CodecFrame)[]) { + for (const one of info) { + const frame = isOggPage(one) ? one.codecFrames[0] : one; + if (frame) { + const rate = frame.header.sampleRate; + if (this.sampleRate === 0) { + this.sampleRate = rate; + break; + } else { + if (rate !== this.sampleRate) { + logger.warn(48); + } + } + } + } + } + + /** + * 解析音频数据 + */ + private async decodeData( + data: Uint8Array, + decoder: AudioDecoder, + parser: CodecParser + ) { + // 解析音频数据 + const audioData = await decoder.decode(data); + if (!audioData) return; + // @ts-expect-error 库类型声明错误 + const audioInfo = [...parser.parseChunk(data)] as ( + | OggPage + | CodecFrame + )[]; + + // 检查采样率 + this.checkSampleRate(audioInfo); + // 追加音频数据 + this.appendDecodedData(audioData, audioInfo); + } + + /** + * 解码剩余数据 + */ + private async decodeFlushData(decoder: AudioDecoder, parser: CodecParser) { + const audioData = await decoder.flush(); + if (!audioData) return; + // @ts-expect-error 库类型声明错误 + const audioInfo = [...parser.flush()] as (OggPage | CodecFrame)[]; + + this.checkSampleRate(audioInfo); + this.appendDecodedData(audioData, audioInfo); + } + + /** + * 追加音频数据 + */ + private appendDecodedData( + data: IAudioDecodeData, + info: (CodecFrame | OggPage)[] + ) { + const channels = data.channelData.length; + if (channels === 0) return; + if (this.audioData.length !== channels) { + this.audioData = []; + for (let i = 0; i < channels; i++) { + this.audioData.push([]); + } + } + // 计算出应该放在哪 + const chunk = this.sampleRate * this.bufferChunkSize; + const sampled = this.bufferedSamples; + const pushIndex = Math.floor(sampled / chunk); + const bufferIndex = sampled % chunk; + const dataLength = data.channelData[0].length; + let buffered = 0; + let nowIndex = pushIndex; + let toBuffer = bufferIndex; + while (buffered < dataLength) { + const rest = toBuffer !== 0 ? chunk - bufferIndex : chunk; + + for (let i = 0; i < channels; i++) { + const audioData = this.audioData[i]; + if (!audioData[nowIndex]) { + audioData.push(new Float32Array(chunk)); + } + const toPush = data.channelData[i].slice( + buffered, + buffered + rest + ); + + audioData[nowIndex].set(toPush, toBuffer); + } + buffered += rest; + nowIndex++; + toBuffer = 0; + } + + this.buffered += + info.reduce((prev, curr) => prev + curr.duration, 0) / 1000; + this.bufferedSamples += info.reduce( + (prev, curr) => prev + curr.samples, + 0 + ); + } + + /** + * 检查已缓冲内容,并在未开始播放时播放 + */ + private checkBufferedPlay() { + if (this.playing || this.sampleRate === 0) return; + const played = this.lastBufferSamples / this.sampleRate; + const dt = this.buffered - played; + if (this.loaded) { + this.playAudio(played); + return; + } + if (dt < this.bufferPlayDuration) return; + this.lastBufferSamples = this.bufferedSamples; + // 需要播放 + this.mergeBuffers(); + if (!this.buffer) return; + if (this.playing) this.output.stop(); + this.createSourceNode(this.buffer); + this.output.loop = false; + this.output.start(0, played); + this.lastStartTime = this.ac.currentTime; + this.playing = true; + this.output.addEventListener('ended', () => { + this.playing = false; + this.checkBufferedPlay(); + }); + } + + private mergeBuffers() { + const buffer = this.ac.createBuffer( + this.audioData.length, + this.bufferedSamples, + this.sampleRate + ); + const chunk = this.sampleRate * this.bufferChunkSize; + const bufferedChunks = Math.floor(this.bufferedSamples / chunk); + const restLength = this.bufferedSamples % chunk; + for (let i = 0; i < this.audioData.length; i++) { + const audio = this.audioData[i]; + const data = new Float32Array(this.bufferedSamples); + for (let j = 0; j < bufferedChunks; j++) { + data.set(audio[j], chunk * j); + } + if (restLength !== 0) { + data.set( + audio[bufferedChunks].slice(0, restLength), + chunk * bufferedChunks + ); + } + + buffer.copyToChannel(data, i, 0); + } + this.buffer = buffer; + } + + async start() { + delete this.buffer; + this.headerRecieved = false; + this.audioType = ''; + this.errored = false; + this.buffered = 0; + this.sampleRate = 0; + this.bufferedSamples = 0; + this.duration = 0; + this.loaded = false; + if (this.playing) this.output.stop(); + this.playing = false; + this.lastStartTime = this.ac.currentTime; + } + + end(done: boolean, reason?: string): void { + if (done && this.buffer) { + this.loaded = true; + delete this.controller; + this.mergeBuffers(); + this.duration = this.buffered; + this.audioData = []; + this.decoder?.destroy(); + delete this.decoder; + delete this.parser; + } else { + logger.warn(44, reason ?? ''); + } + } + + private playAudio(when?: number) { + if (!this.buffer) return; + this.lastStartTime = this.ac.currentTime; + if (this.playing) this.output.stop(); + this.emit('play'); + this.createSourceNode(this.buffer); + this.output.start(0, when); + this.playing = true; + this.output.addEventListener('ended', () => { + this.playing = false; + this.emit('end'); + if (this.loop && !this.output.loop) this.play(0); + }); + } + + play(when?: number): void { + if (this.playing || this.errored) return; + if (this.loaded && this.buffer) { + this.playing = true; + this.playAudio(when); + } else { + this.controller?.start(); + } + } + + private createSourceNode(buffer: AudioBuffer) { + if (!this.target) return; + const node = this.ac.createBufferSource(); + node.buffer = buffer; + if (this.playing) this.output.stop(); + this.playing = false; + this.output = node; + node.connect(this.target.input); + node.loop = this.loop; + } + + stop(): number { + if (this.playing) this.output.stop(); + this.playing = false; + return this.ac.currentTime - this.lastStartTime; + } + + connect(target: IAudioInput): void { + this.target = target; + } + + setLoop(loop: boolean): void { + this.loop = loop; + } +} + +export class AudioElementSource extends AudioSource { + output: MediaElementAudioSourceNode; + + /** audio 元素 */ + readonly audio: HTMLAudioElement; + + get duration(): number { + return this.audio.duration; + } + get currentTime(): number { + return this.audio.currentTime; + } + + constructor(context: AudioContext) { + super(context); + const audio = new Audio(); + audio.preload = 'none'; + this.output = context.createMediaElementSource(audio); + this.audio = audio; + audio.addEventListener('play', () => { + this.playing = true; + this.emit('play'); + }); + audio.addEventListener('ended', () => { + this.playing = false; + this.emit('end'); + }); + } + + /** + * 设置音频源的路径 + * @param url 音频路径 + */ + setSource(url: string) { + this.audio.src = url; + } + + play(when: number = 0): void { + if (this.playing) return; + this.audio.currentTime = when; + this.audio.play(); + } + + stop(): number { + this.audio.pause(); + this.playing = false; + this.emit('end'); + return this.audio.currentTime; + } + + connect(target: IAudioInput): void { + this.output.connect(target.input); + } + + setLoop(loop: boolean): void { + this.audio.loop = loop; + } +} + +export class AudioBufferSource extends AudioSource { + output: AudioBufferSourceNode; + + /** 音频数据 */ + buffer?: AudioBuffer; + /** 是否循环 */ + private loop: boolean = false; + + duration: number = 0; + get currentTime(): number { + return this.ac.currentTime - this.lastStartTime + this.lastStartWhen; + } + + /** 上一次播放是从何时开始的 */ + private lastStartWhen: number = 0; + /** 播放开始时刻 */ + private lastStartTime: number = 0; + private target?: IAudioInput; + + constructor(context: AudioContext) { + super(context); + this.output = context.createBufferSource(); + } + + /** + * 设置音频源数据 + * @param buffer 音频源,可以是未解析的 ArrayBuffer,也可以是已解析的 AudioBuffer + */ + async setBuffer(buffer: ArrayBuffer | AudioBuffer) { + if (buffer instanceof ArrayBuffer) { + this.buffer = await this.ac.decodeAudioData(buffer); + } else { + this.buffer = buffer; + } + this.duration = this.buffer.duration; + } + + play(when?: number): void { + if (this.playing || !this.buffer) return; + this.playing = true; + this.lastStartTime = this.ac.currentTime; + this.emit('play'); + this.createSourceNode(this.buffer); + this.output.start(0, when); + this.output.addEventListener('ended', () => { + this.playing = false; + this.emit('end'); + if (this.loop && !this.output.loop) this.play(0); + }); + } + + private createSourceNode(buffer: AudioBuffer) { + if (!this.target) return; + const node = this.ac.createBufferSource(); + node.buffer = buffer; + this.output = node; + node.connect(this.target.input); + node.loop = this.loop; + } + + stop(): number { + this.output.stop(); + return this.ac.currentTime - this.lastStartTime; + } + + connect(target: IAudioInput): void { + this.target = target; + } + + setLoop(loop: boolean): void { + this.loop = loop; + } +} diff --git a/packages-user/client-modules/src/audio/support.ts b/packages-user/client-modules/src/audio/support.ts new file mode 100644 index 0000000..66ba851 --- /dev/null +++ b/packages-user/client-modules/src/audio/support.ts @@ -0,0 +1,53 @@ +const audio = new Audio(); + +const supportMap = new Map(); + +export const enum AudioType { + Mp3 = 'audio/mpeg', + Wav = 'audio/wav; codecs="1"', + Flac = 'audio/flac', + Opus = 'audio/ogg; codecs="opus"', + Ogg = 'audio/ogg; codecs="vorbis"', + Aac = 'audio/aac' +} + +/** + * 检查一种音频类型是否能被播放 + * @param type 音频类型 + */ +export function isAudioSupport(type: AudioType): boolean { + if (supportMap.has(type)) return supportMap.get(type)!; + else { + const support = audio.canPlayType(type); + const canPlay = support === 'maybe' || support === 'probably'; + supportMap.set(type, canPlay); + return canPlay; + } +} + +const typeMap = new Map([ + ['ogg', AudioType.Ogg], + ['mp3', AudioType.Mp3], + ['wav', AudioType.Wav], + ['flac', AudioType.Flac], + ['opus', AudioType.Opus], + ['aac', AudioType.Aac] +]); + +/** + * 根据文件名拓展猜测其类型 + * @param file 文件名 + */ +export function guessTypeByExt(file: string): AudioType | '' { + const ext = /\.[a-zA-Z\d]+$/.exec(file); + if (!ext?.[0]) return ''; + const type = ext[0].slice(1); + return typeMap.get(type.toLocaleLowerCase()) ?? ''; +} + +isAudioSupport(AudioType.Ogg); +isAudioSupport(AudioType.Mp3); +isAudioSupport(AudioType.Wav); +isAudioSupport(AudioType.Flac); +isAudioSupport(AudioType.Opus); +isAudioSupport(AudioType.Aac); diff --git a/packages-user/client-modules/src/fallback/audio.ts b/packages-user/client-modules/src/fallback/audio.ts new file mode 100644 index 0000000..7a14a14 --- /dev/null +++ b/packages-user/client-modules/src/fallback/audio.ts @@ -0,0 +1,70 @@ +import { Patch, PatchClass } from '@motajs/legacy-common'; +import { audioPlayer, bgmController, soundPlayer } from '../audio'; +import { mainSetting } from '@motajs/legacy-ui'; +import { sleep } from 'mutate-animate'; +import { isNil } from 'lodash-es'; + +// todo: 添加弃用警告 logger.warn(56) + +export function patchAudio() { + const patch = new Patch(PatchClass.Control); + + const play = (bgm: BgmIds, when?: number) => { + bgmController.play(bgm, when); + }; + const pause = () => { + bgmController.pause(); + }; + + patch.add('playBgm', function (bgm, startTime) { + play(bgm, startTime); + }); + patch.add('pauseBgm', function () { + pause(); + }); + patch.add('resumeBgm', function () { + bgmController.resume(); + }); + patch.add('checkBgm', function () { + if (bgmController.playing) return; + if (mainSetting.getValue('audio.bgmEnabled')) { + if (bgmController.playingBgm) { + bgmController.play(bgmController.playingBgm); + } else { + play(main.startBgm, 0); + } + } else { + pause(); + } + }); + patch.add('triggerBgm', function () { + if (bgmController.playing) bgmController.pause(); + else bgmController.resume(); + }); + + patch.add( + 'playSound', + function (sound, _pitch, callback, position, orientation) { + const name = core.getMappedName(sound) as SoundIds; + const num = soundPlayer.play(name, position, orientation); + const route = audioPlayer.getRoute(`sounds.${num}`); + if (!route) { + callback?.(); + return -1; + } else { + sleep(route.duration).then(() => callback?.()); + return num; + } + } + ); + patch.add('stopSound', function (id) { + if (isNil(id)) { + soundPlayer.stopAllSounds(); + } else { + soundPlayer.stop(id); + } + }); + patch.add('getPlayingSounds', function () { + return [...soundPlayer.playing]; + }); +} diff --git a/packages-user/client-modules/src/fallback/index.ts b/packages-user/client-modules/src/fallback/index.ts new file mode 100644 index 0000000..36ce4bf --- /dev/null +++ b/packages-user/client-modules/src/fallback/index.ts @@ -0,0 +1,9 @@ +import { patchAudio } from './audio'; +import { patchWeather } from './weather'; +import { patchUI } from './ui'; + +export function patchAll() { + patchAudio(); + patchWeather(); + patchUI(); +} diff --git a/packages-user/client-modules/src/fallback/ui.ts b/packages-user/client-modules/src/fallback/ui.ts new file mode 100644 index 0000000..60cc772 --- /dev/null +++ b/packages-user/client-modules/src/fallback/ui.ts @@ -0,0 +1,11 @@ +import { Patch, PatchClass } from '@motajs/legacy-common'; +import { TipStore } from '../render/components/tip'; + +export function patchUI() { + const patch = new Patch(PatchClass.UI); + + patch.add('drawTip', function (text, id) { + const tip = TipStore.get('main-tip'); + tip?.drawTip(text, id); + }); +} diff --git a/packages-user/client-modules/src/fallback/weather.ts b/packages-user/client-modules/src/fallback/weather.ts new file mode 100644 index 0000000..e21bfca --- /dev/null +++ b/packages-user/client-modules/src/fallback/weather.ts @@ -0,0 +1,23 @@ +import { Patch, PatchClass } from '@motajs/legacy-common'; +import { WeatherController } from '../weather'; +import { isNil } from 'lodash-es'; + +// todo: 添加弃用警告 logger.warn(56) + +export function patchWeather() { + const patch = new Patch(PatchClass.Control); + let nowWeather: string = ''; + let nowLevel: number = 0; + + patch.add('setWeather', (type, level) => { + const weather = WeatherController.get('main'); + if (!weather) return; + if (type === nowWeather && level === nowLevel) return; + weather.clearWeather(); + if (!isNil(type)) { + weather.activate(type, level); + nowWeather = type; + nowLevel = level ?? 5; + } + }); +} diff --git a/packages-user/client-modules/src/index.ts b/packages-user/client-modules/src/index.ts new file mode 100644 index 0000000..543ed0c --- /dev/null +++ b/packages-user/client-modules/src/index.ts @@ -0,0 +1,20 @@ +import { loading } from '@user/data-base'; +import { createAudio } from './audio'; +import { patchAll } from './fallback'; +import { createGameRenderer, createRender } from './render'; + +export function create() { + createAudio(); + patchAll(); + createRender(); + loading.once('coreInit', () => { + createGameRenderer(); + }); +} + +export * from './action'; +export * from './audio'; +export * from './fallback'; +export * from './loader'; +export * from './render'; +export * from './weather'; diff --git a/packages-user/client-modules/src/loader/index.ts b/packages-user/client-modules/src/loader/index.ts new file mode 100644 index 0000000..3b1a34e --- /dev/null +++ b/packages-user/client-modules/src/loader/index.ts @@ -0,0 +1 @@ +export * from './stream'; diff --git a/packages-user/client-modules/src/loader/stream.ts b/packages-user/client-modules/src/loader/stream.ts new file mode 100644 index 0000000..3a35f0a --- /dev/null +++ b/packages-user/client-modules/src/loader/stream.ts @@ -0,0 +1,128 @@ +import { logger } from '@motajs/common'; +import EventEmitter from 'eventemitter3'; + +export interface IStreamController { + readonly loading: boolean; + + /** + * 开始流传输 + */ + start(): Promise; + + /** + * 主动终止流传输 + * @param reason 终止原因 + */ + cancel(reason?: string): void; +} + +export interface IStreamReader { + /** + * 接受字节流流传输的数据 + * @param data 传入的字节流数据,只包含本分块的内容 + * @param done 是否传输完成 + */ + pump( + data: Uint8Array | undefined, + done: boolean, + response: Response + ): Promise; + + /** + * 当前对象被传递给加载流时执行的函数 + * @param controller 传输流控制对象 + */ + piped(controller: IStreamController): void; + + /** + * 开始流传输 + * @param stream 传输流对象 + * @param controller 传输流控制对象 + */ + start( + stream: ReadableStream, + controller: IStreamController, + response: Response + ): Promise; + + /** + * 结束流传输 + * @param done 是否传输完成,如果为 false 的话,说明可能是由于出现错误导致的终止 + * @param reason 如果没有传输完成,那么表示失败的原因 + */ + end(done: boolean, reason?: string): void; +} + +interface StreamLoaderEvent { + data: [data: Uint8Array | undefined, done: boolean]; +} + +export class StreamLoader + extends EventEmitter + implements IStreamController +{ + /** 传输目标 */ + private target: Set = new Set(); + /** 读取流对象 */ + private stream?: ReadableStream; + + loading: boolean = false; + + constructor(public readonly url: string) { + super(); + } + + /** + * 将加载流传递给字节流读取对象 + * @param reader 字节流读取对象 + */ + pipe(reader: IStreamReader) { + if (this.loading) { + logger.warn(46); + return; + } + this.target.add(reader); + reader.piped(this); + return this; + } + + async start() { + if (this.loading) return; + this.loading = true; + const response = await window.fetch(this.url); + const stream = response.body; + if (!stream) { + logger.error(23); + return; + } + // 获取读取器 + this.stream = stream; + const reader = response.body?.getReader(); + const targets = [...this.target]; + await Promise.all(targets.map(v => v.start(stream, this, response))); + if (reader && reader.read) { + // 开始流传输 + while (true) { + const { value, done } = await reader.read(); + await Promise.all( + targets.map(v => v.pump(value, done, response)) + ); + if (done) break; + } + } else { + // 如果不支持流传输 + const buffer = await response.arrayBuffer(); + const data = new Uint8Array(buffer); + await Promise.all(targets.map(v => v.pump(data, true, response))); + } + this.loading = false; + targets.forEach(v => v.end(true)); + } + + cancel(reason?: string) { + if (!this.stream) return; + this.stream.cancel(reason); + this.loading = false; + this.target.forEach(v => v.end(false, reason)); + } +} diff --git a/packages-user/client-modules/src/render/action.ts b/packages-user/client-modules/src/render/action.ts new file mode 100644 index 0000000..1ce6849 --- /dev/null +++ b/packages-user/client-modules/src/render/action.ts @@ -0,0 +1,13 @@ +import { gameKey } from '@motajs/system-action'; +import { MAIN_WIDTH, MAIN_HEIGHT } from './shared'; +import { saveSave, mainUIController, openStatistics } from './ui'; + +export function createAction() { + gameKey + .realize('save', () => { + saveSave(mainUIController, [0, 0, MAIN_WIDTH, MAIN_HEIGHT]); + }) + .realize('statistics', () => { + openStatistics(mainUIController); + }); +} diff --git a/packages-user/client-modules/src/render/components/choices.tsx b/packages-user/client-modules/src/render/components/choices.tsx new file mode 100644 index 0000000..96c403e --- /dev/null +++ b/packages-user/client-modules/src/render/components/choices.tsx @@ -0,0 +1,638 @@ +import { DefaultProps, ElementLocator, Font } from '@motajs/render'; +import { computed, defineComponent, reactive, ref } from 'vue'; +import { Background, Selection } from './misc'; +import { TextContent, TextContentExpose, TextContentProps } from './textbox'; +import { TextAlign } from './textboxTyper'; +import { Page, PageExpose } from './page'; +import { GameUI, IUIMountable, SetupComponentOptions } from '@motajs/system-ui'; +import { useKey } from '../use'; + +export interface ConfirmBoxProps extends DefaultProps, TextContentProps { + text: string; + width: number; + loc: ElementLocator; + selFont?: Font; + selFill?: CanvasStyle; + pad?: number; + yesText?: string; + noText?: string; + winskin?: ImageIds; + defaultYes?: boolean; + color?: CanvasStyle; + border?: CanvasStyle; +} + +export type ConfirmBoxEmits = { + yes: () => void; + no: () => void; +}; + +const confirmBoxProps = { + props: [ + 'text', + 'width', + 'loc', + 'selFont', + 'selFill', + 'pad', + 'yesText', + 'noText', + 'winskin', + 'defaultYes', + 'color', + 'border' + ], + emits: ['no', 'yes'] +} satisfies SetupComponentOptions< + ConfirmBoxProps, + ConfirmBoxEmits, + keyof ConfirmBoxEmits +>; + +/** + * 确认框组件,与 2.x 的 drawConfirm 类似,可以键盘操作,单次调用参考 {@link getConfirm}。 + * 参数参考 {@link ConfirmBoxProps},事件参考 {@link ConfirmBoxEmits},用例如下: + * ```tsx + * const onYes = () => console.log('yes'); + * const onNo = () => console.log('no'); + * + * + * ``` + */ +export const ConfirmBox = defineComponent< + ConfirmBoxProps, + ConfirmBoxEmits, + keyof ConfirmBoxEmits +>((props, { emit, attrs }) => { + const content = ref(); + const height = ref(200); + const selected = ref(props.defaultYes ? true : false); + const yesSize = ref<[number, number]>([0, 0]); + const noSize = ref<[number, number]>([0, 0]); + + const loc = computed(() => { + const [x = 0, y = 0, , , ax = 0, ay = 0] = props.loc; + return [x, y, props.width, height.value, ax, ay]; + }); + const yesText = computed(() => props.yesText ?? '确认'); + const noText = computed(() => props.noText ?? '取消'); + const pad = computed(() => props.pad ?? 32); + const yesLoc = computed(() => { + const y = height.value - pad.value; + return [props.width / 3, y, void 0, void 0, 0.5, 1]; + }); + const noLoc = computed(() => { + const y = height.value - pad.value; + return [(props.width / 3) * 2, y, void 0, void 0, 0.5, 1]; + }); + const contentLoc = computed(() => { + const width = props.width - pad.value * 2; + return [props.width / 2, pad.value, width, 0, 0.5, 0]; + }); + const selectLoc = computed(() => { + if (selected.value) { + const [x = 0, y = 0] = yesLoc.value; + const [width, height] = yesSize.value; + return [x, y + 4, width + 8, height + 8, 0.5, 1]; + } else { + const [x = 0, y = 0] = noLoc.value; + const [width, height] = noSize.value; + return [x, y + 4, width + 8, height + 8, 0.5, 1]; + } + }); + + const onUpdateHeight = (textHeight: number) => { + height.value = textHeight + pad.value * 4; + }; + + const setYes = (_: string, width: number, height: number) => { + yesSize.value = [width, height]; + }; + + const setNo = (_: string, width: number, height: number) => { + noSize.value = [width, height]; + }; + + const [key] = useKey(); + key.realize('confirm', () => { + if (selected.value) emit('yes'); + else emit('no'); + }); + key.realize('moveLeft', () => void (selected.value = true)); + key.realize('moveRight', () => void (selected.value = false)); + + return () => ( + + + + + emit('yes')} + onEnter={() => (selected.value = true)} + onSetText={setYes} + /> + emit('no')} + onEnter={() => (selected.value = false)} + onSetText={setNo} + /> + + ); +}, confirmBoxProps); + +export type ChoiceKey = string | number | symbol; +export type ChoiceItem = [key: ChoiceKey, text: string]; + +export interface ChoicesProps extends DefaultProps, TextContentProps { + choices: ChoiceItem[]; + loc: ElementLocator; + width: number; + maxHeight?: number; + text?: string; + title?: string; + winskin?: ImageIds; + color?: CanvasStyle; + border?: CanvasStyle; + selFont?: Font; + selFill?: CanvasStyle; + titleFont?: Font; + titleFill?: CanvasStyle; + pad?: number; + interval?: number; +} + +export type ChoicesEmits = { + choose: (key: ChoiceKey) => void; +}; + +const choicesProps = { + props: [ + 'choices', + 'loc', + 'width', + 'maxHeight', + 'text', + 'title', + 'winskin', + 'color', + 'border', + 'selFont', + 'selFill', + 'titleFont', + 'titleFill', + 'pad', + 'interval' + ], + emits: ['choose'] +} satisfies SetupComponentOptions< + ChoicesProps, + ChoicesEmits, + keyof ChoicesEmits +>; + +/** + * 选项框组件,用于在多个选项中选择一个,例如样板的系统设置就由它实现。单次调用参考 {@link getChoice}。 + * 参数参考 {@link ChoicesProps},事件参考 {@link ChoicesEmits}。用例如下: + * ```tsx + * console.log(choice)} + * /> + * ``` + */ +export const Choices = defineComponent< + ChoicesProps, + ChoicesEmits, + keyof ChoicesEmits +>((props, { emit, attrs }) => { + const titleHeight = ref(0); + const contentHeight = ref(0); + const selected = ref(0); + const pageCom = ref(); + const choiceSize = reactive<[number, number][]>([]); + + const selFont = computed(() => props.selFont ?? new Font()); + const maxHeight = computed(() => props.maxHeight ?? 360); + const pad = computed(() => props.pad ?? 28); + const choiceInterval = computed(() => props.interval ?? 16); + const hasText = computed(() => !!props.text); + const hasTitle = computed(() => !!props.title); + const contentWidth = computed(() => props.width - pad.value * 2); + const choiceHeight = computed( + () => selFont.value.size + 8 + choiceInterval.value + ); + const contentY = computed(() => { + if (hasTitle.value) { + return pad.value * 2 + titleHeight.value; + } else { + return pad.value; + } + }); + const choicesY = computed(() => { + const padding = pad.value; + const text = hasText.value; + let y = padding; + if (hasTitle.value) { + y += titleHeight.value; + if (text) { + y += padding / 2; + } else { + y += padding; + } + } + if (text) { + y += contentHeight.value; + y += padding / 2; + } + return y; + }); + const choicesMaxHeight = computed( + () => + maxHeight.value - + choicesY.value - + pad.value * 2 - + selFont.value.size - + 8 + ); + const choiceCountPerPage = computed(() => + Math.max(Math.floor(choicesMaxHeight.value / choiceHeight.value), 1) + ); + const pages = computed(() => + Math.ceil(props.choices.length / choiceCountPerPage.value) + ); + const choicesHeight = computed(() => { + const padBottom = pages.value > 1 ? pad.value + selFont.value.size : 0; + if (props.choices.length > choiceCountPerPage.value) { + return choiceCountPerPage.value * choiceHeight.value + padBottom; + } else { + return props.choices.length * choiceHeight.value + padBottom; + } + }); + const boxHeight = computed(() => { + if (props.choices.length > choiceCountPerPage.value) { + return ( + choicesHeight.value + + choicesY.value + + // 不乘2是因为 choiceY 已经算上了顶部填充 + pad.value + ); + } else { + return ( + choicesHeight.value + + choicesY.value + + // 不乘2是因为 choiceY 已经算上了顶部填充 + pad.value + ); + } + }); + const boxLoc = computed(() => { + const [x = 0, y = 0, , , ax = 0, ay = 0] = props.loc; + return [x, y, props.width, boxHeight.value, ax, ay]; + }); + const titleLoc = computed(() => { + return [props.width / 2, pad.value, void 0, void 0, 0.5, 0]; + }); + const contentLoc = computed(() => { + return [ + props.width / 2, + contentY.value, + contentWidth.value, + void 0, + 0.5, + 0 + ]; + }); + const choiceLoc = computed(() => { + return [ + props.width / 2, + choicesY.value, + contentWidth.value, + choicesHeight.value, + 0.5, + 0 + ]; + }); + const selectionLoc = computed(() => { + const [width = 200, height = 200] = choiceSize[selected.value] ?? []; + return [ + props.width / 2 - pad.value, + (selected.value + 0.5) * choiceHeight.value, + width + 8, + height + 8, + 0.5, + 0.5 + ]; + }); + + const getPageContent = (page: number) => { + const count = choiceCountPerPage.value; + return props.choices.slice(page * count, (page + 1) * count); + }; + + const getChoiceLoc = (index: number): ElementLocator => { + return [ + props.width / 2 - pad.value, + choiceHeight.value * (index + 0.5), + void 0, + void 0, + 0.5, + 0.5 + ]; + }; + + const updateContentHeight = (height: number) => { + contentHeight.value = height; + }; + + const updateTitleHeight = (_0: string, _1: number, height: number) => { + titleHeight.value = height; + }; + + const updateChoiceSize = (index: number, width: number, height: number) => { + choiceSize[index] = [width, height]; + }; + + const onPageChange = () => { + selected.value = 0; + }; + + const [key] = useKey(); + key.realize('moveUp', () => { + if (selected.value === 0) { + if (pageCom.value?.now() !== 0) { + pageCom.value?.movePage(-1); + selected.value = choiceCountPerPage.value - 1; + } + } else { + selected.value--; + } + }); + key.realize('moveDown', () => { + if (selected.value === choiceCountPerPage.value - 1) { + pageCom.value?.movePage(1); + selected.value = 0; + } else { + const page = pageCom.value?.now() ?? 1; + const index = page * choiceCountPerPage.value + selected.value; + if (index < props.choices.length - 1) { + selected.value++; + } + } + }); + key.realize('moveLeft', () => pageCom.value?.movePage(-1)); + key.realize('moveRight', () => pageCom.value?.movePage(1)); + key.realize('confirm', () => { + const page = pageCom.value?.now() ?? 1; + const index = page * choiceCountPerPage.value + selected.value; + emit('choose', props.choices[index][0]); + }); + + return () => ( + +
是
否
加载 index.html
加载 2.x 样板的第三方库
是否在游戏中?
加载渲染端入口
加载数据端入口
初始化数据端,写入 Mota 全局变量
dataRegistered
初始化渲染端
clientRegistered
registered
执行数据端各个模块的初始化函数
执行渲染端各个模块的初始化函数
dataRegistered 与 registered
执行 main.js 初始化
加载全塔属性
加载 core.js 及其他 libs 中的脚本
coreInit
开始资源加载
自动元件加载完毕后触发 autotileLoaded
资源加载完毕后触发 loaded
进入标题界面
请稍候...
资源即将开始加载
HTML5魔塔游戏平台,享受更多魔塔游戏:https://h5mota.com/