feat: 交互事件系统 & refactor: 部分 RenderItem 结构

This commit is contained in:
unanmed 2025-02-18 18:38:21 +08:00
parent 69da048438
commit b448f18616
14 changed files with 1094 additions and 233 deletions

View File

@ -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: {}

View File

@ -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<E extends EContainerEvent = EContainerEvent>
);
}
protected propagateEvent<T extends ActionType>(
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 => {

154
src/core/render/event.ts Normal file
View File

@ -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<IActionEvent>];
/** 当这个元素被点击时的冒泡阶段触发 */
click: [ev: Readonly<IActionEvent>];
/** 当鼠标或手指在该元素上按下的捕获阶段触发 */
downCapture: [ev: Readonly<IActionEvent>];
/** 当鼠标或手指在该元素上按下的冒泡阶段触发 */
down: [ev: Readonly<IActionEvent>];
/** 当鼠标或手指在该元素上移动的捕获阶段触发 */
moveCapture: [ev: Readonly<IActionEvent>];
/** 当鼠标或手指在该元素上移动的冒泡阶段触发 */
move: [ev: Readonly<IActionEvent>];
/** 当鼠标或手指在该元素上抬起的捕获阶段触发 */
upCapture: [ev: Readonly<IActionEvent>];
/** 当鼠标或手指在该元素上抬起的冒泡阶段触发 */
up: [ev: Readonly<IActionEvent>];
/** 当鼠标或手指进入该元素的捕获阶段触发 */
enterCapture: [ev: Readonly<IActionEvent>];
/** 当鼠标或手指进入该元素的冒泡阶段触发 */
enter: [ev: Readonly<IActionEvent>];
/** 当鼠标或手指离开该元素的捕获阶段触发 */
leaveCapture: [ev: Readonly<IActionEvent>];
/** 当鼠标或手指离开该元素的冒泡阶段触发 */
leave: [ev: Readonly<IActionEvent>];
/** 当鼠标滚轮时的捕获阶段触发 */
wheelCapture: [ev: Readonly<IWheelEvent>];
/** 当鼠标滚轮时的冒泡阶段触发 */
wheel: [ev: Readonly<IWheelEvent>];
}
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, string> = {
[ActionType.Click]: 'click',
[ActionType.Down]: 'down',
[ActionType.Move]: 'move',
[ActionType.Up]: 'up',
[ActionType.Enter]: 'enter',
[ActionType.Leave]: 'leave',
[ActionType.Wheel]: 'wheel'
};

View File

@ -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<E extends ERenderItemEvent = ERenderItemEvent>
/** ticker委托id */
static tickerId: number = 0;
/** id到渲染元素的映射 */
static itemMap: Map<string, RenderItem> = 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<E extends ERenderItemEvent = ERenderItemEvent>
/** 不透明度 */
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<RenderItem<ERenderItemEvent>> = 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<E extends ERenderItemEvent = ERenderItemEvent>
readonly enableCache: boolean = true;
/** 是否启用transform下穿机制即画布的变换是否会继续作用到下一层画布 */
readonly transformFallThrough: boolean = false;
//#endregion
//#region 交互事件
/** 是否调用了 `ev.stopPropagation` */
protected propagationStoped: Map<ActionType, boolean> = new Map();
/** 捕获阶段缓存的事件对象 */
private cachedEvent: Map<ActionType, IActionEvent> = new Map();
/** 下穿模式下当前下穿过来的变换矩阵 */
private fallTransform?: Transform;
/** 鼠标当前是否覆盖在当前元素上 */
private hovered: boolean = false;
/** 是否在元素内 */
private inElement: boolean = false;
/** 鼠标标识符映射,键为按下的鼠标按键类型,值表示本次操作的唯一标识符,在按下、移动、抬起过程中保持一致 */
protected mouseId: Map<MouseType, number> = new Map();
/** 当前所有的触摸标识符 */
protected touchId: Set<number> = new Set();
//#endregion
constructor(
type: RenderItemPosition,
@ -369,22 +355,10 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
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<E extends ERenderItemEvent = ERenderItemEvent>
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<E extends ERenderItemEvent = ERenderItemEvent>
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<E extends ERenderItemEvent = ERenderItemEvent>
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<E extends ERenderItemEvent = ERenderItemEvent>
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<any> = 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<E extends ERenderItemEvent = ERenderItemEvent>
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<any> = 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<E extends ERenderItemEvent = ERenderItemEvent>
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<E extends ERenderItemEvent = ERenderItemEvent>
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<E extends ERenderItemEvent = ERenderItemEvent>
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<E extends ERenderItemEvent = ERenderItemEvent>
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<T extends ActionType>(
type: T,
progress: EventProgress,
event: ActionEventMap[T]
): void {
if (progress === EventProgress.Capture) {
this.bubbleEvent(type, event);
} else {
this.parent?.bubbleEvent(type, event);
}
}
private handleEvent<T extends ActionType>(
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<T extends ActionType>(type: T, event: ActionEventMap[T]) {
return this.handleEvent(type, EventProgress.Capture, event);
}
/**
*
* @param type
* @param event
*/
bubbleEvent<T extends ActionType>(type: T, event: ActionEventMap[T]) {
return this.handleEvent(type, EventProgress.Bubble, event);
}
/**
*
* @param type
* @param progress
* @param event
*/
protected processEvent<T extends ActionType>(
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<T extends ActionType>(
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<T extends ActionType>(
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<E extends ERenderItemEvent = ERenderItemEvent>
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<E extends ERenderItemEvent = ERenderItemEvent>
}
}
//#endregion
/**
* 使
*/
@ -819,7 +1069,6 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
this.emit('destroy');
this.removeAllListeners();
this.cache.delete();
RenderItem.itemMap.delete(this._id);
}
}

View File

@ -203,6 +203,7 @@ export class Damage extends RenderItem<EDamageEvent> {
this.cellSize = size;
this.update();
}
/**
* {@link Damage.enemy}
* @param enemy

View File

@ -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;
}
}

View File

@ -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<string, MotaRenderer> = new Map();
/** 所有连接到此根元素的渲染元素的 id 到元素自身的映射 */
protected idMap: Map<string, RenderItem> = new Map();
/** 最后一次按下的鼠标按键,用于处理鼠标移动 */
private lastMouse: MouseType = MouseType.None;
/** 每个触点的信息 */
private touchInfo: Map<number, TouchInfo> = new Map();
/** 触点列表 */
private touchList: Map<number, Touch> = new Map();
/** 每个鼠标按键的信息 */
private mouseInfo: Map<MouseType, MouseInfo> = 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}"></${name}>\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)}</${name}>\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());
};

View File

@ -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<T extends ActionType>(
type: T,
_progress: EventProgress,
event: ActionEventMap[T]
): void {
this.parent?.bubbleEvent(type, event);
}
patchProp(
key: string,
prevValue: any,

View File

@ -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]);

View File

@ -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."
}

View File

@ -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);

View File

@ -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 = '';

View File

@ -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 () => (
<container id="map-draw" {...mapDrawProps}>
<icon icon={50} zIndex={100}></icon>
<layer-group id="layer-main" ex={layerGroupExtends} ref={map}>
<layer layer="bg" zIndex={10}></layer>
<layer layer="bg2" zIndex={20}></layer>

View File

@ -442,6 +442,7 @@ export function getVitualKeyOnce(
assist: number = 0,
emittable: KeyCode[] = []
): Promise<KeyboardEmits> {
// todo: 正确触发后删除监听器
return new Promise(res => {
const key = Keyboard.get('full')!;
key.withAssist(assist);