From b448f1861619648344c7e26871c9d1290fb16848 Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Tue, 18 Feb 2025 18:38:21 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BA=A4=E4=BA=92=E4=BA=8B=E4=BB=B6?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=20&=20refactor:=20=E9=83=A8=E5=88=86=20Rende?= =?UTF-8?q?rItem=20=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pnpm-lock.yaml | 10 +- src/core/render/container.ts | 24 ++ src/core/render/event.ts | 154 +++++++ src/core/render/item.ts | 639 ++++++++++++++++++++--------- src/core/render/preset/damage.ts | 1 + src/core/render/preset/graphics.ts | 77 ++-- src/core/render/render.ts | 399 +++++++++++++++++- src/core/render/sprite.ts | 9 + src/core/render/transform.ts | 6 +- src/data/logger.json | 1 + src/module/fallback/audio.ts | 2 + src/module/fallback/weather.ts | 2 + src/module/render/index.tsx | 2 + src/plugin/utils.ts | 1 + 14 files changed, 1094 insertions(+), 233 deletions(-) create mode 100644 src/core/render/event.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 75b24b5..1e22cea 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2549,8 +2549,8 @@ packages: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} - caniuse-lite@1.0.30001651: - resolution: {integrity: sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==} + caniuse-lite@1.0.30001700: + resolution: {integrity: sha512-2S6XIXwaE7K7erT8dY+kLQcpa5ms63XlRkMkReXjle+kf6c5g38vyMl+Z5y8dSxOFDhcFe+nxnn261PLxBSQsQ==} ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -7241,7 +7241,7 @@ snapshots: autoprefixer@10.4.20(postcss@8.4.49): dependencies: browserslist: 4.23.3 - caniuse-lite: 1.0.30001651 + caniuse-lite: 1.0.30001700 fraction.js: 4.3.7 normalize-range: 0.1.2 picocolors: 1.0.1 @@ -7320,7 +7320,7 @@ snapshots: browserslist@4.23.3: dependencies: - caniuse-lite: 1.0.30001651 + caniuse-lite: 1.0.30001700 electron-to-chromium: 1.5.11 node-releases: 2.0.18 update-browserslist-db: 1.1.0(browserslist@4.23.3) @@ -7395,7 +7395,7 @@ snapshots: camelcase@6.3.0: {} - caniuse-lite@1.0.30001651: {} + caniuse-lite@1.0.30001700: {} ccount@2.0.1: {} diff --git a/src/core/render/container.ts b/src/core/render/container.ts index b43804b..6b36c7e 100644 --- a/src/core/render/container.ts +++ b/src/core/render/container.ts @@ -1,4 +1,5 @@ import { MotaOffscreenCanvas2D } from '../fx/canvas2d'; +import { ActionType, EventProgress, ActionEventMap } from './event'; import { ERenderItemEvent, IRenderChildable, @@ -83,6 +84,29 @@ export class Container ); } + protected propagateEvent( + type: T, + progress: EventProgress, + event: ActionEventMap[T] + ): void { + const len = this.sortedChildren.length; + if (progress === EventProgress.Capture) { + let success = false; + for (let i = len - 1; i >= 0; i--) { + if (this.sortedChildren[i].captureEvent(type, event)) { + success = true; + break; + } + } + // 如果没有子元素能够触发,那么自身触发冒泡 + if (!success) { + this.bubbleEvent(type, event); + } + } else { + this.parent?.bubbleEvent(type, event); + } + } + destroy(): void { super.destroy(); this.children.forEach(v => { diff --git a/src/core/render/event.ts b/src/core/render/event.ts new file mode 100644 index 0000000..41d7b28 --- /dev/null +++ b/src/core/render/event.ts @@ -0,0 +1,154 @@ +import type { RenderItem } from './item'; + +export const enum MouseType { + /** 没有按键按下 */ + None = 0, + /** 左键 */ + Left = 1 << 0, + /** 中键,即按下滚轮 */ + Middle = 1 << 1, + /** 右键 */ + Right = 1 << 2, + /** 侧键后退 */ + Back = 1 << 3, + /** 侧键前进 */ + Forward = 1 << 4 +} + +export const enum WheelType { + None, + /** 以像素为单位 */ + Pixel, + /** 以行为单位,每行长度视浏览器设置而定,约为 1rem */ + Line, + /** 以页为单位,一般为一个屏幕高度 */ + Page +} + +export const enum ActionType { + /** 点击事件,即按下与抬起都在该元素上时触发 */ + Click, + /** 鼠标或手指按下事件 */ + Down, + /** 鼠标或手指移动事件 */ + Move, + /** 鼠标或手指抬起事件 */ + Up, + /** 鼠标或手指移动入该元素时触发的事件 */ + Enter, + /** 鼠标或手指移出该元素时触发的事件 */ + Leave, + /** 鼠标在该元素上滚轮时触发的事件 */ + Wheel +} + +export const enum EventProgress { + /** 捕获阶段 */ + Capture, + /** 冒泡阶段 */ + Bubble +} + +export interface IActionEvent { + /** 当前事件是监听的哪个元素 */ + target: RenderItem; + /** 这次操作的标识符,在按下、移动、抬起阶段中保持不变 */ + identifier: number; + /** 是触摸操作还是鼠标操作 */ + touch: boolean; + /** 相对于触发元素左上角的横坐标 */ + offsetX: number; + /** 相对于触发元素左上角的纵坐标 */ + offsetY: number; + /** 相对于整个画布左上角的横坐标 */ + absoluteX: number; + /** 相对于整个画布左上角的纵坐标 */ + absoluteY: number; + /** + * 触发的按键种类,会出现在点击、按下、抬起三个事件中,而其他的如移动等该值只会是 {@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; + + /** + * 调用后将停止事件的继续传播。 + * 在捕获阶段,将会阻止捕获的进一步进行,在冒泡阶段,将会阻止冒泡的进一步进行。 + * 如果当前元素有很多监听器,该方法并不会阻止其他监听器的执行。 + */ + stopPropagation(): void; +} + +export interface IWheelEvent extends IActionEvent { + /** 滚轮事件的鼠标横向滚动量 */ + wheelX: number; + /** 滚轮事件的鼠标纵向滚动量 */ + wheelY: number; + /** 滚轮事件的鼠标垂直屏幕的滚动量 */ + wheelZ: number; + /** 滚轮事件的滚轮类型,表示了对应值的单位 */ + wheelType: WheelType; +} + +export interface ERenderItemActionEvent { + /** 当这个元素被点击时的捕获阶段触发 */ + clickCapture: [ev: Readonly]; + /** 当这个元素被点击时的冒泡阶段触发 */ + click: [ev: Readonly]; + /** 当鼠标或手指在该元素上按下的捕获阶段触发 */ + downCapture: [ev: Readonly]; + /** 当鼠标或手指在该元素上按下的冒泡阶段触发 */ + down: [ev: Readonly]; + /** 当鼠标或手指在该元素上移动的捕获阶段触发 */ + moveCapture: [ev: Readonly]; + /** 当鼠标或手指在该元素上移动的冒泡阶段触发 */ + move: [ev: Readonly]; + /** 当鼠标或手指在该元素上抬起的捕获阶段触发 */ + upCapture: [ev: Readonly]; + /** 当鼠标或手指在该元素上抬起的冒泡阶段触发 */ + up: [ev: Readonly]; + /** 当鼠标或手指进入该元素的捕获阶段触发 */ + enterCapture: [ev: Readonly]; + /** 当鼠标或手指进入该元素的冒泡阶段触发 */ + enter: [ev: Readonly]; + /** 当鼠标或手指离开该元素的捕获阶段触发 */ + leaveCapture: [ev: Readonly]; + /** 当鼠标或手指离开该元素的冒泡阶段触发 */ + leave: [ev: Readonly]; + /** 当鼠标滚轮时的捕获阶段触发 */ + wheelCapture: [ev: Readonly]; + /** 当鼠标滚轮时的冒泡阶段触发 */ + wheel: [ev: Readonly]; +} + +export interface ActionEventMap { + [ActionType.Click]: IActionEvent; + [ActionType.Down]: IActionEvent; + [ActionType.Enter]: IActionEvent; + [ActionType.Leave]: IActionEvent; + [ActionType.Move]: IActionEvent; + [ActionType.Up]: IActionEvent; + [ActionType.Wheel]: IWheelEvent; +} + +export const eventNameMap: Record = { + [ActionType.Click]: 'click', + [ActionType.Down]: 'down', + [ActionType.Move]: 'move', + [ActionType.Up]: 'up', + [ActionType.Enter]: 'enter', + [ActionType.Leave]: 'leave', + [ActionType.Wheel]: 'wheel' +}; diff --git a/src/core/render/item.ts b/src/core/render/item.ts index 45def3e..32c47fa 100644 --- a/src/core/render/item.ts +++ b/src/core/render/item.ts @@ -6,6 +6,16 @@ import { Transform } from './transform'; import { logger } from '../common/logger'; import { ElementNamespace, ComponentInternalInstance } from 'vue'; import { transformCanvas } from './utils'; +import { + ActionEventMap, + ActionType, + ERenderItemActionEvent, + eventNameMap, + EventProgress, + IActionEvent, + MouseType +} from './event'; +import { vec3 } from 'gl-matrix'; export type RenderFunction = ( canvas: MotaOffscreenCanvas2D, @@ -140,114 +150,39 @@ export interface IRenderVueSupport { ): void; } -export const enum MouseType { - /** 没有按键按下 */ - None = 0, - /** 左键 */ - Left = 1 << 0, - /** 中键,即按下滚轮 */ - Middle = 1 << 1, - /** 右键 */ - Right = 1 << 2, - /** 侧键后退 */ - Back = 1 << 3, - /** 侧键前进 */ - Forward = 1 << 4 -} - -export const enum WheelType { - None, - /** 以像素为单位 */ - Pixel, - /** 以行为单位,每行长度视浏览器设置而定,约为 1rem */ - Line, - /** 以页为单位,一般为一个屏幕高度 */ - Page -} - -export interface IActionEvent { - /** 当前事件是监听的哪个元素 */ - readonly target: RenderItem; - /** 这次操作的标识符,在按下、移动、抬起阶段中保持不变 */ - readonly identifier: number; - /** 相对于触发元素左上角的横坐标 */ - readonly offsetX: number; - /** 相对于触发元素左上角的纵坐标 */ - readonly offsetY: number; - /** 相对于整个画布左上角的横坐标 */ - readonly absoluteX: number; - /** 相对于整个画布左上角的纵坐标 */ - readonly absoluteY: number; - /** - * 触发的按键种类,会出现在点击、按下、抬起三个事件中,而其他的如移动等该值只会是 {@link MouseType.None}, - * 电脑端可以有左键、中键、右键等,手机只会触发左键,每一项的值参考 {@link MouseType} - */ - readonly type: MouseType; - /** - * 当前按下了哪些按键。该值是一个数字,可以通过位运算判断是否按下了某个按键。 - * 例如通过 `buttons & MouseType.Left` 来判断是否按下了左键。 - */ - readonly buttons: number; - /** 触发时是否按下了 alt 键 */ - readonly altKey: boolean; - /** 触发时是否按下了 shift 键 */ - readonly shiftKey: boolean; - /** 触发时是否按下了 ctrl 键 */ - readonly ctrlKey: boolean; - /** 触发时是否按下了 Windows(Windows) / Command(Mac) 键 */ - readonly metaKey: boolean; +export interface IRenderTreeRoot { + readonly isRoot: true; /** - * 调用后将停止事件的继续传播。 - * 在捕获阶段,将会阻止捕获的进一步进行,在冒泡阶段,将会阻止冒泡的进一步进行。 - * 如果当前元素有很多监听器,该方法并不会阻止其他监听器的执行。 + * 将一个渲染元素连接到此根元素 + * @param item 要连接到此根元素的渲染元素 */ - stopPropagation(): void; + connect(item: RenderItem): void; + + /** + * 将已连接的渲染元素从此根元素中去掉 + * @param item 要取消连接的渲染元素 + */ + disconnect(item: RenderItem): void; + + /** + * 修改已连接的元素的 id + * @param item 修改了 id 的元素 + * @param previous 先前的元素 id + * @param current 现在的元素 id + */ + modifyId(item: RenderItem, previous: string, current: string): void; + + /** + * 获取渲染至的目标画布,即显示在画面上的画布 + */ + getCanvas(): HTMLCanvasElement; } -export interface IWheelEvent extends IActionEvent { - /** 滚轮事件的鼠标横向滚动量 */ - readonly wheelX: number; - /** 滚轮事件的鼠标纵向滚动量 */ - readonly wheelY: number; - /** 滚轮事件的鼠标垂直屏幕的滚动量 */ - readonly wheelZ: number; - /** 滚轮事件的滚轮类型,表示了对应值的单位 */ - readonly wheelType: WheelType; -} - -export interface ERenderItemEvent { +export interface ERenderItemEvent extends ERenderItemActionEvent { beforeRender: [transform: Transform]; afterRender: [transform: Transform]; destroy: []; - /** 当这个元素被点击时的捕获阶段触发 */ - clickCapture: [ev: IActionEvent]; - /** 当这个元素被点击时的冒泡阶段触发 */ - click: [ev: IActionEvent]; - /** 当鼠标或手指在该元素上按下的捕获阶段触发 */ - downCapture: [ev: IActionEvent]; - /** 当鼠标或手指在该元素上按下的冒泡阶段触发 */ - down: [ev: IActionEvent]; - /** 当鼠标或手指在该元素上移动的捕获阶段触发 */ - moveCapture: [ev: IActionEvent]; - /** 当鼠标或手指在该元素上移动的冒泡阶段触发 */ - move: [ev: IActionEvent]; - /** 当鼠标或手指在该元素上抬起的捕获阶段触发 */ - upCapture: [ev: IActionEvent]; - /** 当鼠标或手指在该元素上抬起的冒泡阶段触发 */ - up: [ev: IActionEvent]; - /** 当鼠标或手指进入该元素的捕获阶段触发 */ - enterCapture: [ev: IActionEvent]; - /** 当鼠标或手指进入该元素的冒泡阶段触发 */ - enter: [ev: IActionEvent]; - /** 当鼠标或手指离开该元素的捕获阶段触发 */ - leaveCapture: [ev: IActionEvent]; - /** 当鼠标或手指离开该元素的冒泡阶段触发 */ - leave: [ev: IActionEvent]; - /** 当鼠标滚轮时的捕获阶段触发 */ - wheelCapture: [ev: IWheelEvent]; - /** 当鼠标滚轮时的冒泡阶段触发 */ - wheel: [ev: IWheelEvent]; } interface TickerDelegation { @@ -281,25 +216,22 @@ export abstract class RenderItem /** ticker委托id */ static tickerId: number = 0; - /** id到渲染元素的映射 */ - static itemMap: Map = new Map(); - readonly uid: number = count++; - private _id: string = ''; + //#region 元素属性 + private _id: string = ''; + /** + * 元素的 id,原则上不可重复 + */ get id(): string { return this._id; } set id(v: string) { - if (this.isRoot || this.findRoot()) { - if (RenderItem.itemMap.has(this._id)) { - logger.warn(23, this._id); - RenderItem.itemMap.delete(this._id); - } - RenderItem.itemMap.set(v, this); - } + this.checkRoot(); + const prev = this._id; this._id = v; + this._root?.modifyId(this, prev, v); } /** 元素纵深,表示了遮挡关系 */ @@ -328,27 +260,61 @@ export abstract class RenderItem /** 不透明度 */ alpha: number = 1; + get x() { + return this._transform.x; + } + get y() { + return this._transform.y; + } + + /** 该元素的变换矩阵 */ + private _transform: Transform = new Transform(); + set transform(value: Transform) { + this._transform.bind(); + this._transform = value; + value.bind(this); + } + get transform() { + return this._transform; + } + + private _cursor: string = 'auto'; + /** 鼠标覆盖在该元素上时的指针样式 */ + set cursor(v: string) { + this.setCursor(v); + } + get cursor() { + return this._cursor; + } + + //#endregion + + //#region 父子关系 + private _parent?: RenderItem; /** 当前元素的父元素 */ get parent() { return this._parent; } - /** 当前元素是否为根元素 */ + /** 当前元素是否为根元素,如果是根元素,那么必须实现 `IRenderTreeRoot` 接口 */ readonly isRoot: boolean = false; - /** 该元素的变换矩阵 */ - transform: Transform = new Transform(); + private _root?: RenderItem & IRenderTreeRoot; + get root() { + return this._root; + } + + /** 当前元素是否已经连接至任意根元素 */ + get connected() { + return !!this._root; + } /** 该渲染元素的子元素 */ children: Set> = new Set(); - get x() { - return this.transform.x; - } - get y() { - return this.transform.y; - } + //#endregion + //#region 渲染配置与缓存 /** 渲染缓存信息 */ protected cache: MotaOffscreenCanvas2D = new MotaOffscreenCanvas2D(); /** 是否需要更新缓存 */ @@ -357,6 +323,26 @@ export abstract class RenderItem readonly enableCache: boolean = true; /** 是否启用transform下穿机制,即画布的变换是否会继续作用到下一层画布 */ readonly transformFallThrough: boolean = false; + //#endregion + + //#region 交互事件 + + /** 是否调用了 `ev.stopPropagation` */ + protected propagationStoped: Map = new Map(); + /** 捕获阶段缓存的事件对象 */ + private cachedEvent: Map = new Map(); + /** 下穿模式下当前下穿过来的变换矩阵 */ + private fallTransform?: Transform; + /** 鼠标当前是否覆盖在当前元素上 */ + private hovered: boolean = false; + /** 是否在元素内 */ + private inElement: boolean = false; + /** 鼠标标识符映射,键为按下的鼠标按键类型,值表示本次操作的唯一标识符,在按下、移动、抬起过程中保持一致 */ + protected mouseId: Map = new Map(); + /** 当前所有的触摸标识符 */ + protected touchId: Set = new Set(); + + //#endregion constructor( type: RenderItemPosition, @@ -369,22 +355,10 @@ export abstract class RenderItem this.transformFallThrough = transformFallThrough; this.type = type; - this.transform.bind(this); + this._transform.bind(this); this.cache.withGameScale(true); } - private findRoot() { - let ele: RenderItem = this; - while (!ele.isRoot) { - if (!ele.parent) { - return null; - } else { - ele = ele.parent; - } - } - return ele; - } - /** * 渲染函数 * @param canvas 渲染至的画布 @@ -398,25 +372,18 @@ export abstract class RenderItem transform: Transform ): void; - /** - * 修改这个对象的大小 - */ - size(width: number, height: number): void { - this.width = width; - this.height = height; - this.cache.size(width, height); - this.update(this); - } - /** * 渲染当前对象 * @param canvas 渲染至的画布 - * @param transform 父元素的变换矩阵 + * @param transform 由父元素传递过来的变换矩阵 */ renderContent(canvas: MotaOffscreenCanvas2D, transform: Transform) { if (this.hidden) return; this.emit('beforeRender', transform); - const tran = this.transformFallThrough ? transform : this.transform; + if (this.transformFallThrough) { + this.fallTransform = transform; + } + const tran = this.transformFallThrough ? transform : this._transform; const ax = -this.anchorX * this.width; const ay = -this.anchorY * this.height; @@ -424,8 +391,8 @@ export abstract class RenderItem const ctx = canvas.ctx; ctx.save(); canvas.setAntiAliasing(this.antiAliasing); - if (this.enableCache) canvas.ctx.filter = this.filter; if (this.type === 'static') transformCanvas(canvas, tran); + ctx.filter = this.filter; ctx.globalAlpha = this.alpha; ctx.globalCompositeOperation = this.composite; if (this.enableCache) { @@ -447,13 +414,25 @@ export abstract class RenderItem this.emit('afterRender', transform); } + //#region 修改元素属性 + + /** + * 修改这个对象的大小 + */ + size(width: number, height: number): void { + this.width = width; + this.height = height; + this.cache.size(width, height); + this.update(this); + } + /** * 设置这个元素的位置,等效于`transform.setTranslate(x, y)` * @param x 横坐标 * @param y 纵坐标 */ pos(x: number, y: number) { - this.transform.setTranslate(x, y); + this._transform.setTranslate(x, y); this.update(); } @@ -484,32 +463,6 @@ export abstract class RenderItem this.update(); } - /** - * 获取当前元素的绝对位置(不建议使用,因为应当很少会有获取绝对位置的需求) - */ - getAbsolutePosition(): LocArr { - if (this.type === 'absolute') return [0, 0]; - const { x, y } = this.transform; - if (!this.parent) return [x, y]; - else { - const [px, py] = this.parent.getAbsolutePosition(); - return [x + px, y + py]; - } - } - - setAnchor(x: number, y: number): void { - this.anchorX = x; - this.anchorY = y; - this.update(); - } - - update(item: RenderItem = this): void { - if (this.cacheDirty) return; - this.cacheDirty = true; - if (this.hidden) return; - this.parent?.update(item); - } - setHD(hd: boolean): void { this.highResolution = hd; this.cache.setHD(hd); @@ -527,6 +480,70 @@ export abstract class RenderItem this.parent?.requestSort(); } + setAnchor(x: number, y: number): void { + this.anchorX = x; + this.anchorY = y; + this.update(); + } + + /** + * 设置鼠标放在该元素上时的指针样式 + * @param cursor 要设置成的指针样式 + */ + setCursor(cursor: string = 'auto') { + const canvas = this._root?.getCanvas(); + if (!canvas) return; + if (this.hovered) { + canvas.style.cursor = cursor; + } + this._cursor = cursor; + } + + /** + * 隐藏这个元素 + */ + hide() { + if (this.hidden) return; + this.hidden = true; + this.update(this); + } + + /** + * 显示这个元素 + */ + show() { + if (!this.hidden) return; + this.hidden = false; + this.refreshAllChildren(); + } + + //#endregion + + /** + * 获取当前元素的绝对位置(不建议使用,因为应当很少会有获取绝对位置的需求) + */ + getAbsolutePosition(x: number = 0, y: number = 0): LocArr { + if (this.type === 'absolute') { + if (this.parent) return this.parent.getAbsolutePosition(0, 0); + else return [0, 0]; + } + const [px, py] = this._transform.transformed(x, y); + if (!this.parent) return [px, py]; + else { + const [px, py] = this.parent.getAbsolutePosition(); + return [x + px, y + py]; + } + } + + update(item: RenderItem = this): void { + if (this.cacheDirty) return; + this.cacheDirty = true; + if (this.hidden) return; + this.parent?.update(item); + } + + //#region 动画帧与 ticker + requestBeforeFrame(fn: () => void): void { beforeFrame.push(fn); } @@ -570,22 +587,22 @@ export abstract class RenderItem return RenderItem.tickerMap.has(id); } - /** - * 隐藏这个元素 - */ - hide() { - if (this.hidden) return; - this.hidden = true; - this.update(this); - } + //#endregion - /** - * 显示这个元素 - */ - show() { - if (!this.hidden) return; - this.hidden = false; - this.refreshAllChildren(); + //#region 父子关系 + + protected checkRoot() { + if (this._root || this.isRoot) return this._root; + let ele: RenderItem = this; + while (!ele.isRoot) { + if (!ele.parent) { + return null; + } else { + ele = ele.parent; + } + } + this._root = ele as RenderItem & IRenderTreeRoot; + return ele; } /** @@ -615,9 +632,8 @@ export abstract class RenderItem parent.requestSort(); this.update(); if (this._id !== '') { - const root = this.findRoot(); - if (!root) return; - RenderItem.itemMap.set(this._id, this); + this.checkRoot(); + this._root?.connect(this); } } @@ -633,7 +649,8 @@ export abstract class RenderItem parent.requestSort(); parent.update(); if (!success) return false; - RenderItem.itemMap.delete(this._id); + this._root?.disconnect(this); + this._root = void 0; return true; } @@ -660,6 +677,237 @@ export abstract class RenderItem logger.warn(37); } + //#endregion + + //#region 交互事件 + + /** + * 根据事件类型和事件阶段获取事件名称 + * @param type 事件类型 + * @param progress 事件阶段 + */ + getEventName( + type: ActionType, + progress: EventProgress + ): keyof ERenderItemActionEvent { + if (progress === EventProgress.Capture) { + return `${eventNameMap[type]}Capture` as keyof ERenderItemActionEvent; + } else { + return eventNameMap[type] as keyof ERenderItemActionEvent; + } + } + + /** + * 传递事件,即将事件传递给父元素或子元素等,可以通过 override 来实现自己的事件传递, + * 例如 Container 元素就需要在捕获阶段将事件传递给所有子元素, + * 默认行为是,捕获阶段触发自身冒泡,冒泡阶段触发父元素冒泡,适用于大部分不包含子元素的元素 + * @param type 事件类型 + * @param progress 事件阶段,捕获阶段或冒泡阶段 + * @param event 正在处理的事件对象 + */ + protected propagateEvent( + type: T, + progress: EventProgress, + event: ActionEventMap[T] + ): void { + if (progress === EventProgress.Capture) { + this.bubbleEvent(type, event); + } else { + this.parent?.bubbleEvent(type, event); + } + } + + private handleEvent( + type: T, + progress: EventProgress, + event: ActionEventMap[T] + ) { + const ev = this.processEvent(type, progress, event); + if (ev) { + const name = this.getEventName(type, progress); + this.emit(name, ev); + if (!this.propagationStoped.get(type)) { + this.propagateEvent(type, progress, ev); + } + } + this.propagationStoped.set(type, false); + return ev; + } + + /** + * 捕获事件 + * @param type 事件类型 + * @param event 由父元素传递来的事件 + */ + captureEvent(type: T, event: ActionEventMap[T]) { + return this.handleEvent(type, EventProgress.Capture, event); + } + + /** + * 冒泡事件 + * @param type 事件类型 + * @param event 由子元素传递来的事件 + */ + bubbleEvent(type: T, event: ActionEventMap[T]) { + return this.handleEvent(type, EventProgress.Bubble, event); + } + + /** + * 处理事件,用于根据上一级传递的事件内容生成新的事件内容,并执行一些事件的默认行为 + * @param type 事件类型 + * @param progress 事件阶段,捕获阶段还是冒泡阶段 + * @param event 由上一级(捕获阶段的父元素,冒泡阶段的子元素)传递来的事件内容 + */ + protected processEvent( + type: T, + progress: EventProgress, + event: ActionEventMap[T] + ): ActionEventMap[T] | null { + if (progress === EventProgress.Capture) { + // 捕获阶段需要计算鼠标位置 + const tran = this.transformFallThrough + ? this.fallTransform + : this._transform; + if (!tran) return null; + const [nx, ny] = this.calActionPosition(event, tran); + const inElement = this.isActionInElement(nx, ny); + // 在元素范围内,执行事件 + const newEvent: ActionEventMap[T] = { + ...event, + offsetX: nx, + offsetY: ny, + target: this, + stopPropagation: () => { + this.propagationStoped.set(type, true); + } + }; + this.inElement = inElement; + if (!this.processCapture(type, newEvent, inElement)) return null; + this.cachedEvent.set(type, newEvent); + return newEvent; + } else { + const newEvent = this.cachedEvent.get(type) as ActionEventMap[T]; + this.processBubble(type, newEvent, this.inElement); + this.cachedEvent.delete(type); + return newEvent; + } + } + + /** + * 处理捕获阶段的事件,可以通过 override 来添加新内容,注意调用 `super.processCapture` 来执行默认行为 + * @param type 事件类型 + * @param event 正在处理的事件对象 + * @param inElement 当前鼠标是否在元素内 + * @returns 是否继续传递事件 + */ + protected processCapture( + type: T, + event: ActionEventMap[T], + inElement: boolean + ): boolean { + switch (type) { + case ActionType.Move: { + if (this.hovered && !inElement) { + this.hovered = false; + this.emit('leaveCapture', event); + this.emit('leave', event); + return false; + } else if (!this.hovered && inElement) { + this.hovered = true; + this.emit('enterCapture', event); + this.emit('enter', event); + return true; + } + break; + } + case ActionType.Down: { + // 记录标识符,用于判定 click + if (event.touch) { + this.touchId.add(event.identifier); + } else { + this.mouseId.set(event.type, event.identifier); + } + break; + } + case ActionType.Click: { + if (event.touch) { + if (!this.touchId.has(event.identifier)) { + return false; + } + this.touchId.delete(event.identifier); + } else { + if (this.mouseId.get(event.type) !== event.identifier) { + this.mouseId.delete(event.type); + return false; + } + this.mouseId.delete(event.type); + } + break; + } + case ActionType.Enter: { + const canvas = this._root?.getCanvas(); + if (!canvas) return true; + canvas.style.cursor = this._cursor; + } + } + + return inElement; + } + + /** + * 处理冒泡阶段的事件,可以通过 override 来添加新内容,注意调用 `super.processBubble` 来执行默认行为 + * @param type 事件类型 + * @param event 正在处理的事件对象 + * @param inElement 当前鼠标是否在元素内 + * @returns 是否继续传递事件 + */ + protected processBubble( + type: T, + event: ActionEventMap[T], + inElement: boolean + ): boolean { + return inElement; + } + + /** + * 计算一个点击事件在该元素上的位置 + * @param event 触发的事件 + * @param transform 当前的变换矩阵 + */ + protected calActionPosition( + event: IActionEvent, + transform: Transform + ): vec3 { + return transform.untransformed(event.offsetX, event.offsetY); + } + + /** + * 判断一个点击事件是否在元素内,可以通过 override 来修改其行为 + * @param x 横坐标 + * @param y 纵坐标 + */ + protected isActionInElement(x: number, y: number) { + return x >= 0 && x < this.width && y >= 0 && y < this.height; + } + + actionClick() {} + + actionDown() {} + + actionUp() {} + + actionMove() {} + + actionEnter() {} + + actionLeave() {} + + actionWheel() {} + + //#endregion + + //#region vue支持 props处理 + /** * 判断一个prop是否是期望类型 * @param value 实际值 @@ -720,12 +968,12 @@ export abstract class RenderItem switch (key) { case 'x': { if (!this.assertType(nextValue, 'number', key)) return; - this.pos(nextValue, this.transform.y); + this.pos(nextValue, this._transform.y); return; } case 'y': { if (!this.assertType(nextValue, 'number', key)) return; - this.pos(this.transform.x, nextValue); + this.pos(this._transform.x, nextValue); return; } case 'anchorX': { @@ -811,6 +1059,8 @@ export abstract class RenderItem } } + //#endregion + /** * 摧毁这个渲染元素,摧毁后不应继续使用 */ @@ -819,7 +1069,6 @@ export abstract class RenderItem this.emit('destroy'); this.removeAllListeners(); this.cache.delete(); - RenderItem.itemMap.delete(this._id); } } diff --git a/src/core/render/preset/damage.ts b/src/core/render/preset/damage.ts index d1d09b8..2044a2d 100644 --- a/src/core/render/preset/damage.ts +++ b/src/core/render/preset/damage.ts @@ -203,6 +203,7 @@ export class Damage extends RenderItem { this.cellSize = size; this.update(); } + /** * 更新怪物列表。更新后,{@link Damage.enemy} 会丢失原来的怪物列表引用,换为传入的列表引用 * @param enemy 怪物列表 diff --git a/src/core/render/preset/graphics.ts b/src/core/render/preset/graphics.ts index def6470..d37c7a7 100644 --- a/src/core/render/preset/graphics.ts +++ b/src/core/render/preset/graphics.ts @@ -79,6 +79,54 @@ export abstract class GraphicItemBase private strokeAndFill: boolean = false; private propFillSet: boolean = false; + /** + * 获取这个元素的绘制路径 + */ + abstract getPath(): Path2D; + + protected render( + canvas: MotaOffscreenCanvas2D, + _transform: Transform + ): void { + const ctx = canvas.ctx; + this.setCanvasState(canvas); + const path = this.getPath(); + switch (this.mode) { + case GraphicMode.Fill: + ctx.fill(path, this.fillRule); + break; + case GraphicMode.Stroke: + ctx.stroke(path); + break; + case GraphicMode.FillAndStroke: + ctx.fill(path, this.fillRule); + ctx.stroke(path); + break; + case GraphicMode.StrokeAndFill: + ctx.stroke(path); + ctx.fill(path, this.fillRule); + break; + } + } + + protected isActionInElement(x: number, y: number): boolean { + const ctx = this.cache.ctx; + const path = this.getPath(); + switch (this.mode) { + case GraphicMode.Fill: + return ctx.isPointInPath(path, x, y, this.fillRule); + case GraphicMode.Stroke: + return ctx.isPointInStroke(path, x, y); + case GraphicMode.FillAndStroke: + case GraphicMode.StrokeAndFill: + return ( + ctx.isPointInPath(path, x, y, this.fillRule) || + ctx.isPointInStroke(path, x, y) + ); + } + return false; + } + /** * 设置描边绘制的信息 * @param options 线的信息 @@ -253,31 +301,10 @@ export abstract class GraphicItemBase } export class Rect extends GraphicItemBase { - protected render( - canvas: MotaOffscreenCanvas2D, - _transform: Transform - ): void { - const ctx = canvas.ctx; - this.setCanvasState(canvas); - ctx.beginPath(); - ctx.rect(this.x, this.y, this.width, this.height); - - switch (this.mode) { - case GraphicMode.Fill: - ctx.fill(this.fillRule); - break; - case GraphicMode.Stroke: - ctx.stroke(); - break; - case GraphicMode.FillAndStroke: - ctx.fill(this.fillRule); - ctx.stroke(); - break; - case GraphicMode.StrokeAndFill: - ctx.stroke(); - ctx.fill(this.fillRule); - break; - } + getPath(): Path2D { + const path = new Path2D(); + path.rect(this.x, this.y, this.width, this.height); + return path; } } diff --git a/src/core/render/render.ts b/src/core/render/render.ts index 4b0e4b2..bbf3452 100644 --- a/src/core/render/render.ts +++ b/src/core/render/render.ts @@ -1,15 +1,55 @@ import { logger } from '../common/logger'; import { MotaOffscreenCanvas2D } from '../fx/canvas2d'; import { Container } from './container'; -import { RenderItem } from './item'; +import { + ActionType, + IActionEvent, + IWheelEvent, + MouseType, + WheelType +} from './event'; +import { IRenderTreeRoot, RenderItem } from './item'; import { Transform } from './transform'; -export class MotaRenderer extends Container { +interface TouchInfo { + /** 这次触摸在渲染系统的标识符 */ + identifier: number; + /** 浏览器的 clientX,用于判断这个触点有没有移动 */ + clientX: number; + /** 浏览器的 clientY,用于判断这个触点有没有移动 */ + clientY: number; + /** 是否覆盖在了当前元素上 */ + hovered: boolean; +} + +interface MouseInfo { + /** 这个鼠标按键的标识符 */ + identifier: number; +} + +export class MotaRenderer extends Container implements IRenderTreeRoot { static list: Map = new Map(); + /** 所有连接到此根元素的渲染元素的 id 到元素自身的映射 */ + protected idMap: Map = new Map(); + + /** 最后一次按下的鼠标按键,用于处理鼠标移动 */ + private lastMouse: MouseType = MouseType.None; + /** 每个触点的信息 */ + private touchInfo: Map = new Map(); + /** 触点列表 */ + private touchList: Map = new Map(); + /** 每个鼠标按键的信息 */ + private mouseInfo: Map = new Map(); + /** 操作的标识符 */ + private actionIdentifier: number = 0; + + /** 用于终止 document 上的监听 */ + private abort?: AbortController; + target!: MotaOffscreenCanvas2D; - readonly isRoot: boolean = true; + readonly isRoot = true; constructor(id: string = 'render-main') { super('static', false); @@ -38,6 +78,297 @@ export class MotaRenderer extends Container { }; update(); + this.listen(); + } + + private listen() { + // 画布监听 + const canvas = this.target.canvas; + canvas.addEventListener('mousedown', ev => { + const mouse = this.getMouseType(ev); + this.lastMouse = mouse; + this.captureEvent( + ActionType.Down, + this.createMouseAction(ev, ActionType.Down, mouse) + ); + }); + canvas.addEventListener('mouseup', ev => { + const event = this.createMouseAction(ev, ActionType.Up); + this.captureEvent(ActionType.Up, event); + this.captureEvent(ActionType.Click, event); + }); + canvas.addEventListener('mousemove', ev => { + const event = this.createMouseAction( + ev, + ActionType.Move, + this.lastMouse + ); + this.captureEvent(ActionType.Move, event); + }); + canvas.addEventListener('mouseenter', ev => { + const event = this.createMouseAction(ev, ActionType.Enter); + this.emit('enterCapture', event); + this.emit('enter', event); + }); + canvas.addEventListener('mouseleave', ev => { + const event = this.createMouseAction(ev, ActionType.Leave); + this.emit('leaveCapture', event); + this.emit('leave', event); + }); + document.addEventListener('touchstart', ev => { + this.createTouchAction(ev, ActionType.Down).forEach(v => { + this.captureEvent(ActionType.Down, v); + }); + }); + document.addEventListener('touchend', ev => { + this.createTouchAction(ev, ActionType.Up).forEach(v => { + this.captureEvent(ActionType.Up, v); + this.captureEvent(ActionType.Click, v); + }); + }); + document.addEventListener('touchcancel', ev => { + this.createTouchAction(ev, ActionType.Up).forEach(v => { + this.captureEvent(ActionType.Up, v); + }); + }); + document.addEventListener('touchmove', ev => { + this.createTouchAction(ev, ActionType.Move).forEach(v => { + const touch = this.touchInfo.get(v.identifier); + if (!touch) return; + const inElement = this.isTouchInCanvas(v.offsetX, v.offsetY); + if (touch.hovered && !inElement) { + this.emit('leaveCapture', v); + this.emit('leave', v); + } + if (!touch.hovered && inElement) { + this.emit('enterCapture', v); + this.emit('enter', v); + } + this.captureEvent(ActionType.Move, v); + }); + }); + canvas.addEventListener('wheel', ev => { + this.captureEvent( + ActionType.Wheel, + this.createWheelAction(ev, ActionType.Wheel) + ); + }); + // 文档监听 + const abort = new AbortController(); + const signal = abort.signal; + this.abort = abort; + const clear = (ev: MouseEvent) => { + const mouse = this.getMouseButtons(ev); + for (const button of this.mouseInfo.keys()) { + if (!(mouse & button)) { + this.mouseInfo.delete(button); + } + } + }; + document.addEventListener('click', clear, { signal }); + document.addEventListener('mouseenter', clear, { signal }); + document.addEventListener('mouseleave', clear, { signal }); + } + + private isTouchInCanvas(clientX: number, clientY: number) { + const rect = this.target.canvas.getBoundingClientRect(); + const { left, right, top, bottom } = rect; + const x = clientX; + const y = clientY; + return x >= left && x <= right && y >= top && y <= bottom; + } + + private getMouseType(ev: MouseEvent): MouseType { + switch (ev.button) { + case 0: + return MouseType.Left; + case 1: + return MouseType.Middle; + case 2: + return MouseType.Right; + case 3: + return MouseType.Back; + case 4: + return MouseType.Forward; + } + return MouseType.None; + } + + private getActiveMouseIdentifier(mouse: MouseType) { + if (this.lastMouse === MouseType.None) { + return -1; + } else { + const info = this.mouseInfo.get(mouse); + if (!info) return -1; + else return info.identifier; + } + } + + private getMouseIdentifier(type: ActionType, mouse: MouseType): number { + switch (type) { + case ActionType.Down: { + const id = this.actionIdentifier++; + this.mouseInfo.set(mouse, { identifier: id }); + return id; + } + case ActionType.Move: + case ActionType.Enter: + case ActionType.Leave: + case ActionType.Wheel: + case ActionType.Up: + case ActionType.Click: { + const id = this.getActiveMouseIdentifier(mouse); + this.mouseInfo.delete(mouse); + return id; + } + } + } + + private getMouseButtons(event: MouseEvent): number { + if (event.buttons === 0) return MouseType.None; + let buttons = 0; + if (event.buttons & 0b1) buttons |= MouseType.Left; + if (event.buttons & 0b10) buttons |= MouseType.Right; + if (event.buttons & 0b100) buttons |= MouseType.Middle; + if (event.buttons & 0b1000) buttons |= MouseType.Back; + if (event.buttons & 0b10000) buttons |= MouseType.Forward; + return buttons; + } + + private createMouseAction( + event: MouseEvent, + type: ActionType, + mouse: MouseType = this.getMouseType(event) + ): IActionEvent { + const id = this.getMouseIdentifier(type, mouse); + return { + target: this, + identifier: id, + touch: false, + offsetX: event.offsetX, + offsetY: event.offsetY, + absoluteX: event.offsetX, + absoluteY: event.offsetY, + type: mouse, + buttons: this.getMouseButtons(event), + altKey: event.altKey, + ctrlKey: event.ctrlKey, + shiftKey: event.shiftKey, + metaKey: event.metaKey, + stopPropagation: () => { + this.propagationStoped.set(type, true); + } + }; + } + + private createWheelAction( + event: WheelEvent, + type: ActionType, + mouse: MouseType = this.getMouseType(event) + ): IWheelEvent { + const ev = this.createMouseAction(event, type, mouse) as IWheelEvent; + ev.wheelX = event.deltaX; + ev.wheelY = event.deltaY; + ev.wheelZ = event.deltaZ; + switch (event.deltaMode) { + case 0x00: + ev.wheelType = WheelType.Pixel; + break; + case 0x01: + ev.wheelType = WheelType.Line; + break; + case 0x02: + ev.wheelType = WheelType.Page; + break; + default: + ev.wheelType = WheelType.None; + break; + } + return ev; + } + + private getTouchIdentifier(touch: Touch, type: ActionType) { + if (type === ActionType.Down) { + const id = this.actionIdentifier++; + this.touchInfo.set(touch.identifier, { + identifier: id, + clientX: touch.clientX, + clientY: touch.clientY, + hovered: this.isTouchInCanvas(touch.clientX, touch.clientY) + }); + return id; + } + const info = this.touchInfo.get(touch.identifier); + if (!info) return -1; + return info.identifier; + } + + private createTouch( + touch: Touch, + type: ActionType, + event: TouchEvent, + rect: DOMRect + ): IActionEvent { + const x = touch.clientX - rect.left; + const y = touch.clientY - rect.top; + return { + target: this, + identifier: this.getTouchIdentifier(touch, type), + touch: true, + offsetX: x, + offsetY: y, + absoluteX: x, + absoluteY: y, + type: MouseType.Left, + buttons: MouseType.Left, + altKey: event.altKey, + ctrlKey: event.ctrlKey, + shiftKey: event.shiftKey, + metaKey: event.metaKey, + stopPropagation: () => { + this.propagationStoped.set(type, true); + } + }; + } + + private createTouchAction( + event: TouchEvent, + type: ActionType + ): IActionEvent[] { + const list: IActionEvent[] = []; + const rect = this.target.canvas.getBoundingClientRect(); + if (type === ActionType.Up) { + // 抬起是一个需要特殊处理的东西,因为 touches 不会包含这个内容,所以需要特殊处理 + const touches = Array.from(event.touches).map(v => v.identifier); + for (const [id, touch] of this.touchList) { + if (!touches.includes(id)) { + // 如果不包含,才需要触发 + if (this.isTouchInCanvas(touch.clientX, touch.clientY)) { + const ev = this.createTouch(touch, type, event, rect); + list.push(ev); + } + } + } + } else { + Array.from(event.touches).forEach(v => { + const ev = this.createTouch(v, type, event, rect); + if (type === ActionType.Move) { + const touch = this.touchInfo.get(v.identifier); + if (!touch) return; + const moveX = touch.clientX - v.clientX; + const moveY = touch.clientY - v.clientY; + if (moveX !== 0 || moveY !== 0) { + list.push(ev); + } + } else if (type === ActionType.Down) { + this.touchList.set(v.identifier, v); + if (this.isTouchInCanvas(v.clientX, v.clientY)) { + list.push(ev); + } + } + }); + } + return list; } update(_item: RenderItem = this) { @@ -56,13 +387,13 @@ export class MotaRenderer extends Container { * @returns */ getElementById(id: string): RenderItem | null { - const map = RenderItem.itemMap; - const item = map.get(id); + if (id.length === 0) return null; + const item = this.idMap.get(id); if (item) return item; else { const item = this.searchElement(this, id); if (item) { - map.set(id, item); + this.idMap.set(id, item); } return item; } @@ -79,10 +410,61 @@ export class MotaRenderer extends Container { return null; } + connect(item: RenderItem): void { + if (item.id.length === 0) return; + if (this.idMap.has(item.id)) { + logger.warn(23, item.id); + } else { + this.idMap.set(item.id, item); + } + } + + disconnect(item: RenderItem): void { + this.idMap.delete(item.id); + } + + modifyId(item: RenderItem, previous: string, current: string): void { + this.idMap.delete(previous); + if (current.length !== 0) { + if (this.idMap.has(item.id)) { + logger.warn(23, item.id); + } else { + this.idMap.set(item.id, item); + } + } + } + + getCanvas(): HTMLCanvasElement { + return this.target.canvas; + } + destroy() { super.destroy(); MotaRenderer.list.delete(this.id); this.target.delete(); + this.abort?.abort(); + } + + private toTagString(item: RenderItem, space: number, deep: number): string { + const name = item.constructor.name; + if (item.children.size === 0) { + return `${' '.repeat(deep * space)}<${name} id="${item.id}" type="${item.type}">\n`; + } else { + return ( + `${' '.repeat(deep * space)}<${name} id="${item.id}" type="${item.type}">\n` + + `${[...item.children].map(v => this.toTagString(v, space, deep + 1)).join('')}` + + `${' '.repeat(deep * space)}\n` + ); + } + } + + /** + * 调试功能,将渲染树输出为 XML 标签形式,只包含渲染元素类名,以及元素 id 等基础属性,不包含属性值等 + * @param space 缩进空格数 + */ + toTagTree(space: number = 4) { + if (!import.meta.env.DEV) return ''; + return this.toTagString(this, space, 0); } static get(id: string) { @@ -95,3 +477,8 @@ window.addEventListener('resize', () => { v.requestAfterFrame(() => v.refreshAllChildren()) ); }); + +// @ts-expect-error debug +window.logTagTree = () => { + console.log(MotaRenderer.get('render-main')?.toTagTree()); +}; diff --git a/src/core/render/sprite.ts b/src/core/render/sprite.ts index db8c5e9..ddb177f 100644 --- a/src/core/render/sprite.ts +++ b/src/core/render/sprite.ts @@ -7,6 +7,7 @@ import { import { MotaOffscreenCanvas2D } from '../fx/canvas2d'; import { Transform } from './transform'; import { ElementNamespace, ComponentInternalInstance } from 'vue'; +import { ActionType, EventProgress, ActionEventMap } from './event'; export interface ESpriteEvent extends ERenderItemEvent {} @@ -42,6 +43,14 @@ export class Sprite< this.update(this); } + protected propagateEvent( + type: T, + _progress: EventProgress, + event: ActionEventMap[T] + ): void { + this.parent?.bubbleEvent(type, event); + } + patchProp( key: string, prevValue: any, diff --git a/src/core/render/transform.ts b/src/core/render/transform.ts index 24c99d0..cf525ef 100644 --- a/src/core/render/transform.ts +++ b/src/core/render/transform.ts @@ -205,7 +205,8 @@ export class Transform { * @param x 横坐标 * @param y 纵坐标 */ - transformed(x: number, y: number) { + transformed(x: number, y: number): vec3 { + if (!this.modified) return [x, y, 1]; return multiplyVec3(this.mat, [x, y, 1]); } @@ -214,7 +215,8 @@ export class Transform { * @param x 横坐标 * @param y 纵坐标 */ - untransformed(x: number, y: number) { + untransformed(x: number, y: number): vec3 { + if (!this.modified) return [x, y, 1]; const invert = mat3.create(); mat3.invert(invert, this.mat); return multiplyVec3(invert, [x, y, 1]); diff --git a/src/data/logger.json b/src/data/logger.json index 1946679..0b49000 100644 --- a/src/data/logger.json +++ b/src/data/logger.json @@ -87,6 +87,7 @@ "53": "Cannot $1 audio route '$2', since there is not added route named it.", "54": "Missing start tag for '$1', index: $2.", "55": "Unchildable tag '$1' should follow with param.", + "56": "Method '$1' is deprecated. Consider using '$2' instead.", "1001": "Item-detail extension needs 'floor-binder' and 'floor-damage' extension as dependency.", "1101": "Cannot add new effect to point effect instance, for there's no more reserve space for it. Please increase the max count of the instance." } diff --git a/src/module/fallback/audio.ts b/src/module/fallback/audio.ts index 1f0509c..a678034 100644 --- a/src/module/fallback/audio.ts +++ b/src/module/fallback/audio.ts @@ -4,6 +4,8 @@ import { mainSetting } from '@/core/main/setting'; import { sleep } from 'mutate-animate'; import { isNil } from 'lodash-es'; +// todo: 添加弃用警告 logger.warn(56) + export function patchAudio() { const patch = new Patch(PatchClass.Control); diff --git a/src/module/fallback/weather.ts b/src/module/fallback/weather.ts index 76b05fb..d8fb989 100644 --- a/src/module/fallback/weather.ts +++ b/src/module/fallback/weather.ts @@ -2,6 +2,8 @@ import { Patch, PatchClass } from '@/common/patch'; 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 = ''; diff --git a/src/module/render/index.tsx b/src/module/render/index.tsx index a216599..af7f88a 100644 --- a/src/module/render/index.tsx +++ b/src/module/render/index.tsx @@ -17,6 +17,7 @@ import { Textbox } from './components'; import { ILayerGroupRenderExtends, ILayerRenderExtends } from '@/core/render'; import { Props } from '@/core/render'; import { WeatherController } from '../weather'; +import { IActionEvent } from '@/core/render/event'; export function create() { const main = new MotaRenderer(); @@ -65,6 +66,7 @@ export function create() { return () => ( + diff --git a/src/plugin/utils.ts b/src/plugin/utils.ts index 6f7e262..fa947d5 100644 --- a/src/plugin/utils.ts +++ b/src/plugin/utils.ts @@ -442,6 +442,7 @@ export function getVitualKeyOnce( assist: number = 0, emittable: KeyCode[] = [] ): Promise { + // todo: 正确触发后删除监听器 return new Promise(res => { const key = Keyboard.get('full')!; key.withAssist(assist);