mirror of
https://github.com/motajs/template.git
synced 2026-09-18 13:10:21 +08:00
898 lines
27 KiB
TypeScript
898 lines
27 KiB
TypeScript
import { logger } from '@motajs/common';
|
||
import { MotaOffscreenCanvas2D } from './canvas2d';
|
||
import { Container, CustomContainer } from './container';
|
||
import {
|
||
ActionType,
|
||
IActionEvent,
|
||
IActionEventBase,
|
||
IWheelEvent,
|
||
MouseType,
|
||
WheelType
|
||
} from './event';
|
||
import {
|
||
IMotaRendererConfig,
|
||
RenderItemTags,
|
||
IRenderItem,
|
||
IRenderItemInstanceMap,
|
||
IRenderItemParameterMap,
|
||
IRenderTreeRoot,
|
||
RenderItemConstructor,
|
||
ExcitableDelegation
|
||
} from './types';
|
||
import { IExcitable, IExcitation, RafExcitation } from '@motajs/animate';
|
||
import { CustomRenderItem } from './custom';
|
||
import { Comment, Image, Text } from './misc';
|
||
import { Shader } from './shader';
|
||
import {
|
||
BezierCurve,
|
||
Circle,
|
||
Ellipse,
|
||
Line,
|
||
Path,
|
||
QuadraticCurve,
|
||
Rect,
|
||
RectR
|
||
} from './graphics';
|
||
|
||
interface TouchInfo {
|
||
/** 这次触摸在渲染系统的标识符 */
|
||
readonly identifier: number;
|
||
/** 浏览器的 clientX,用于判断这个触点有没有移动 */
|
||
readonly clientX: number;
|
||
/** 浏览器的 clientY,用于判断这个触点有没有移动 */
|
||
readonly clientY: number;
|
||
/** 是否覆盖在了当前元素上 */
|
||
readonly hovered: boolean;
|
||
}
|
||
|
||
interface MouseInfo {
|
||
/** 这个鼠标按键的标识符 */
|
||
readonly identifier: number;
|
||
}
|
||
|
||
interface DelegatedExcitable extends IExcitable<number> {
|
||
/** 委托内容的 id */
|
||
readonly id: number;
|
||
/** 委托内容的原始对象 */
|
||
readonly obj: ExcitableDelegation;
|
||
/** 委托 excitable 是否永久执行 */
|
||
readonly forever: boolean;
|
||
/** 委托 excitable 的开始时刻 */
|
||
readonly startTime: number;
|
||
/** 委托 excitable 的持续时间 */
|
||
readonly time: number;
|
||
/** 委托结束时执行的函数 */
|
||
readonly end?: () => void;
|
||
}
|
||
|
||
export class MotaRenderer
|
||
extends Container
|
||
implements IRenderTreeRoot, IExcitable<number>
|
||
{
|
||
/** 所有连接到此根元素的渲染元素的 id 到元素自身的映射 */
|
||
protected readonly idMap: Map<string, IRenderItem> = 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;
|
||
/** 根据捕获行为判断光标样式 */
|
||
private targetCursor: string = 'auto';
|
||
/** 当前鼠标覆盖的元素 */
|
||
private hoveredElement: Set<IRenderItem> = new Set();
|
||
/** 本次交互前鼠标覆盖的元素 */
|
||
private beforeHovered: Set<IRenderItem> = new Set();
|
||
|
||
/** 渲染至的目标画布 */
|
||
readonly target!: MotaOffscreenCanvas2D;
|
||
/** 当前元素是根元素 */
|
||
readonly isRoot = true;
|
||
|
||
/** 当前元素的激励源 */
|
||
readonly excitation!: IExcitation<number>;
|
||
|
||
/** 下一帧之前需要执行的内容 */
|
||
private readonly beforeFrame: Set<() => void> = new Set();
|
||
/** 下一帧之后需要执行的内容 */
|
||
private readonly afterFrame: Set<() => void> = new Set();
|
||
/** 委托 excitables 的计数器 */
|
||
private delegationCounter: number = 0;
|
||
/** 委托执行的 excitables */
|
||
private readonly excitables: Map<number, DelegatedExcitable> = new Map();
|
||
/** 委托执行的 excitables 到其 id 的映射 */
|
||
private readonly excitablesMap: Map<
|
||
ExcitableDelegation,
|
||
DelegatedExcitable
|
||
> = new Map();
|
||
/** 执行完毕需要删除的 excitables */
|
||
private readonly toDeleteExcitables: Set<number> = new Set();
|
||
|
||
/** 标签注册信息 */
|
||
private readonly tagRegistry: Map<string, RenderItemConstructor> =
|
||
new Map();
|
||
|
||
constructor(config: IMotaRendererConfig) {
|
||
super(false);
|
||
|
||
const canvas = this.getMountCanvas(config.canvas);
|
||
if (!canvas) {
|
||
logger.error(19);
|
||
return;
|
||
}
|
||
this.target = new MotaOffscreenCanvas2D(config.alpha ?? true, canvas);
|
||
this.size(config.width, config.height);
|
||
this.target.setAntiAliasing(false);
|
||
if (config.excitaion) {
|
||
this.excitation = config.excitaion;
|
||
} else {
|
||
this.excitation = new RafExcitation();
|
||
}
|
||
|
||
this.setAnchor(0.5, 0.5);
|
||
this.listen();
|
||
this.setScale(1);
|
||
this.excited = this.excited.bind(this);
|
||
this.excitation.add(this);
|
||
this.registerIntrinsicTags();
|
||
}
|
||
|
||
//#region 渲染相关
|
||
|
||
excited(payload: number): void {
|
||
this.beforeFrame.forEach(v => v());
|
||
this.beforeFrame.clear();
|
||
this.refresh();
|
||
this.afterFrame.forEach(v => v());
|
||
this.afterFrame.clear();
|
||
|
||
this.excitables.forEach((ex, key) => {
|
||
if (!ex.forever && payload - ex.startTime >= ex.time) {
|
||
this.toDeleteExcitables.add(key);
|
||
ex.end?.();
|
||
} else {
|
||
ex.excited(payload);
|
||
}
|
||
});
|
||
this.toDeleteExcitables.forEach(key => this.excitables.delete(key));
|
||
}
|
||
|
||
private getMountCanvas(
|
||
canvas: string | HTMLCanvasElement
|
||
): HTMLCanvasElement | undefined {
|
||
if (typeof canvas === 'string') {
|
||
return document.querySelector(canvas) as HTMLCanvasElement;
|
||
} else {
|
||
return canvas;
|
||
}
|
||
}
|
||
|
||
update(_item: IRenderItem = this) {
|
||
this.cacheDirty = true;
|
||
}
|
||
|
||
protected refresh(): void {
|
||
if (!this.cacheDirty) return;
|
||
this.target.clear();
|
||
this.renderContent(this.target);
|
||
}
|
||
|
||
getCanvas(): HTMLCanvasElement {
|
||
return this.target.canvas;
|
||
}
|
||
|
||
//#endregion
|
||
|
||
//#region 标签元素
|
||
|
||
createElement<K extends RenderItemTags>(
|
||
tag: K,
|
||
...params: IRenderItemParameterMap[K]
|
||
): IRenderItemInstanceMap[K];
|
||
createElement<P extends any[], I extends IRenderItem>(
|
||
ele: new (...params: P) => I,
|
||
...params: P
|
||
): I;
|
||
createElement(
|
||
ele: RenderItemTags | RenderItemConstructor,
|
||
...params: any[]
|
||
): IRenderItem {
|
||
if (typeof ele === 'string') {
|
||
const Cons = this.tagRegistry.get(ele);
|
||
if (!Cons) {
|
||
logger.error(15, ele);
|
||
return new Container(false);
|
||
} else {
|
||
return new Cons(...params);
|
||
}
|
||
} else {
|
||
return new ele(...params);
|
||
}
|
||
}
|
||
|
||
registerElement(tag: string, cons: RenderItemConstructor): void {
|
||
if (this.tagRegistry.has(tag)) {
|
||
logger.error(14);
|
||
return;
|
||
} else {
|
||
this.tagRegistry.set(tag, cons);
|
||
}
|
||
}
|
||
|
||
hasTag(tag: string): boolean {
|
||
return this.tagRegistry.has(tag);
|
||
}
|
||
|
||
/**
|
||
* 注册内置渲染标签
|
||
*/
|
||
private registerIntrinsicTags() {
|
||
this.registerElement('container', Container);
|
||
this.registerElement('custom', CustomRenderItem);
|
||
this.registerElement('text', Text);
|
||
this.registerElement('image', Image);
|
||
this.registerElement('shader', Shader);
|
||
this.registerElement('comment', Comment);
|
||
this.registerElement('template', Container);
|
||
this.registerElement('custom-container', CustomContainer);
|
||
this.registerElement('g-rect', Rect);
|
||
this.registerElement('g-circle', Circle);
|
||
this.registerElement('g-ellipse', Ellipse);
|
||
this.registerElement('g-line', Line);
|
||
this.registerElement('g-bezier', BezierCurve);
|
||
this.registerElement('g-quad', QuadraticCurve);
|
||
this.registerElement('g-path', Path);
|
||
this.registerElement('g-rectr', RectR);
|
||
}
|
||
|
||
//#endregion
|
||
|
||
//#region 尺寸缩放
|
||
|
||
/**
|
||
* 设置这个渲染器的缩放比
|
||
* @param scale 缩放比
|
||
*/
|
||
setScale(scale: number) {
|
||
this.onResize(scale);
|
||
}
|
||
|
||
getScale() {
|
||
return this.target.scale;
|
||
}
|
||
|
||
onResize(scale: number): void {
|
||
this.target.setScale(scale);
|
||
const width = this.target.width * scale;
|
||
const height = this.target.height * scale;
|
||
this.target.canvas.style.width = `${width}px`;
|
||
this.target.canvas.style.height = `${height}px`;
|
||
super.onResize(scale);
|
||
}
|
||
|
||
size(width: number, height: number): void {
|
||
super.size(width, height);
|
||
this.target.size(width, height);
|
||
this.transform.setTranslate(width / 2, height / 2);
|
||
}
|
||
|
||
//#endregion
|
||
|
||
//#region 事件处理
|
||
|
||
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.targetCursor = 'auto';
|
||
const temp = this.beforeHovered;
|
||
temp.clear();
|
||
this.beforeHovered = this.hoveredElement;
|
||
this.hoveredElement = temp;
|
||
this.captureEvent(ActionType.Move, event);
|
||
if (this.targetCursor !== this.target.canvas.style.cursor) {
|
||
this.target.canvas.style.cursor = this.targetCursor;
|
||
}
|
||
this.checkMouseEnterLeave(
|
||
ev,
|
||
event,
|
||
this.beforeHovered,
|
||
this.hoveredElement
|
||
);
|
||
});
|
||
canvas.addEventListener('mouseleave', ev => {
|
||
const id = this.getMouseIdentifier(
|
||
ActionType.Leave,
|
||
this.getMouseType(ev)
|
||
);
|
||
this.hoveredElement.forEach(v => {
|
||
v.emit('leave', this.createMouseActionBase(ev, id, v));
|
||
});
|
||
this.hoveredElement.clear();
|
||
this.beforeHovered.clear();
|
||
});
|
||
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 });
|
||
window.addEventListener(
|
||
'resize',
|
||
() => {
|
||
this.requestAfterFrame(() => this.refreshAllChildren());
|
||
},
|
||
{ signal }
|
||
);
|
||
document.addEventListener(
|
||
'touchstart',
|
||
ev => {
|
||
this.createTouchAction(ev, ActionType.Down).forEach(v => {
|
||
this.captureEvent(ActionType.Down, v);
|
||
});
|
||
},
|
||
{ signal }
|
||
);
|
||
document.addEventListener(
|
||
'touchend',
|
||
ev => {
|
||
this.createTouchAction(ev, ActionType.Up).forEach(v => {
|
||
this.captureEvent(ActionType.Up, v);
|
||
this.captureEvent(ActionType.Click, v);
|
||
});
|
||
[...ev.touches].forEach(v => {
|
||
this.touchInfo.delete(v.identifier);
|
||
});
|
||
},
|
||
{ signal }
|
||
);
|
||
document.addEventListener(
|
||
'touchcancel',
|
||
ev => {
|
||
this.createTouchAction(ev, ActionType.Up).forEach(v => {
|
||
this.captureEvent(ActionType.Up, v);
|
||
});
|
||
[...ev.touches].forEach(v => {
|
||
this.touchInfo.delete(v.identifier);
|
||
});
|
||
},
|
||
{ signal }
|
||
);
|
||
document.addEventListener(
|
||
'touchmove',
|
||
ev => {
|
||
this.createTouchAction(ev, ActionType.Move).forEach(v => {
|
||
const list = this.touchInfo.values();
|
||
if (!list.some(vv => v.identifier === vv.identifier)) {
|
||
return;
|
||
}
|
||
const temp = this.beforeHovered;
|
||
temp.clear();
|
||
this.beforeHovered = this.hoveredElement;
|
||
this.hoveredElement = temp;
|
||
this.captureEvent(ActionType.Move, v);
|
||
this.checkTouchEnterLeave(
|
||
ev,
|
||
v,
|
||
this.beforeHovered,
|
||
this.hoveredElement
|
||
);
|
||
});
|
||
},
|
||
{ 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: {
|
||
return this.getActiveMouseIdentifier(mouse);
|
||
}
|
||
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 createMouseActionBase(
|
||
event: MouseEvent,
|
||
id: number,
|
||
target: IRenderItem = this,
|
||
mouse: MouseType = this.getMouseType(event)
|
||
): IActionEventBase {
|
||
return {
|
||
identifier: id,
|
||
target: target,
|
||
touch: false,
|
||
type: mouse,
|
||
buttons: this.getMouseButtons(event),
|
||
altKey: event.altKey,
|
||
ctrlKey: event.ctrlKey,
|
||
shiftKey: event.shiftKey,
|
||
metaKey: event.metaKey
|
||
};
|
||
}
|
||
|
||
private createTouchActionBase(
|
||
event: TouchEvent,
|
||
id: number,
|
||
target: IRenderItem
|
||
): IActionEventBase {
|
||
return {
|
||
identifier: id,
|
||
target: target,
|
||
touch: false,
|
||
type: MouseType.Left,
|
||
buttons: MouseType.Left,
|
||
altKey: event.altKey,
|
||
ctrlKey: event.ctrlKey,
|
||
shiftKey: event.shiftKey,
|
||
metaKey: event.metaKey
|
||
};
|
||
}
|
||
|
||
private createMouseAction(
|
||
event: MouseEvent,
|
||
type: ActionType,
|
||
mouse: MouseType = this.getMouseType(event)
|
||
): IActionEvent {
|
||
const id = this.getMouseIdentifier(type, mouse);
|
||
const x = event.offsetX / this.scale;
|
||
const y = event.offsetY / this.scale;
|
||
|
||
return {
|
||
target: this,
|
||
identifier: id,
|
||
touch: false,
|
||
offsetX: x,
|
||
offsetY: y,
|
||
absoluteX: x,
|
||
absoluteY: y,
|
||
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) / this.scale;
|
||
const y = (touch.clientY - rect.top) / this.scale;
|
||
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;
|
||
}
|
||
|
||
private checkMouseEnterLeave(
|
||
event: MouseEvent,
|
||
ev: IActionEvent,
|
||
before: Set<IRenderItem>,
|
||
now: Set<IRenderItem>
|
||
) {
|
||
// 先 leave,再 enter
|
||
before.forEach(v => {
|
||
if (!now.has(v)) {
|
||
v.emit(
|
||
'leave',
|
||
this.createMouseActionBase(event, ev.identifier, v)
|
||
);
|
||
}
|
||
});
|
||
now.forEach(v => {
|
||
if (!before.has(v)) {
|
||
v.emit(
|
||
'enter',
|
||
this.createMouseActionBase(event, ev.identifier, v)
|
||
);
|
||
}
|
||
});
|
||
}
|
||
|
||
private checkTouchEnterLeave(
|
||
event: TouchEvent,
|
||
ev: IActionEvent,
|
||
before: Set<IRenderItem>,
|
||
now: Set<IRenderItem>
|
||
) {
|
||
// 先 leave,再 enter
|
||
before.forEach(v => {
|
||
if (!now.has(v)) {
|
||
v.emit(
|
||
'leave',
|
||
this.createTouchActionBase(event, ev.identifier, v)
|
||
);
|
||
}
|
||
});
|
||
now.forEach(v => {
|
||
if (!before.has(v)) {
|
||
v.emit(
|
||
'enter',
|
||
this.createTouchActionBase(event, ev.identifier, v)
|
||
);
|
||
}
|
||
});
|
||
}
|
||
|
||
//#endregion
|
||
|
||
//#region 元素处理
|
||
|
||
/**
|
||
* 根据渲染元素的id获取一个渲染元素
|
||
* @param id 要获取的渲染元素id
|
||
* @returns
|
||
*/
|
||
getElementById(id: string): IRenderItem | null {
|
||
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) {
|
||
this.idMap.set(id, item);
|
||
}
|
||
return item;
|
||
}
|
||
}
|
||
|
||
private searchElement(ele: IRenderItem, id: string): IRenderItem | null {
|
||
for (const child of ele.children) {
|
||
if (child.id === id) return child;
|
||
else {
|
||
const ele = this.searchElement(child, id);
|
||
if (ele) return ele;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
connect(item: IRenderItem): void {
|
||
if (item.id.length === 0) return;
|
||
const existed = this.idMap.get(item.id);
|
||
if (existed) {
|
||
if (existed === item) return;
|
||
else logger.warn(23, item.id);
|
||
} else {
|
||
this.idMap.set(item.id, item);
|
||
}
|
||
}
|
||
|
||
disconnect(item: IRenderItem): void {
|
||
this.idMap.delete(item.id);
|
||
}
|
||
|
||
modifyId(item: IRenderItem, 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);
|
||
}
|
||
}
|
||
}
|
||
|
||
hoverElement(element: IRenderItem): void {
|
||
if (element.cursor !== 'inherit') {
|
||
this.targetCursor = element.cursor;
|
||
}
|
||
this.hoveredElement.add(element);
|
||
}
|
||
|
||
//#endregion
|
||
|
||
//#region 渲染绑定
|
||
|
||
requestBeforeFrame(fn: () => void): void {
|
||
this.beforeFrame.add(fn);
|
||
}
|
||
|
||
requestAfterFrame(fn: () => void): void {
|
||
this.afterFrame.add(fn);
|
||
}
|
||
|
||
delegateExcitable(
|
||
fn: ExcitableDelegation,
|
||
time?: number,
|
||
end?: () => void
|
||
): number {
|
||
const index = this.delegationCounter++;
|
||
const info: DelegatedExcitable = {
|
||
id: index,
|
||
obj: fn,
|
||
excited: typeof fn === 'function' ? fn : fn.excited,
|
||
startTime: this.excitation.payload(),
|
||
time: time ?? 0,
|
||
forever: time === void 0,
|
||
end
|
||
};
|
||
this.excitables.set(index, info);
|
||
this.excitablesMap.set(fn, info);
|
||
return index;
|
||
}
|
||
|
||
removeExcitable(id: number, callEnd: boolean = true): boolean {
|
||
const info = this.excitables.get(id);
|
||
if (!info) return false;
|
||
if (callEnd) {
|
||
info.end?.();
|
||
}
|
||
this.excitables.delete(id);
|
||
this.excitablesMap.delete(info.obj);
|
||
return true;
|
||
}
|
||
|
||
removeExcitableObject(
|
||
excitable: ExcitableDelegation,
|
||
callEnd: boolean = true
|
||
): boolean {
|
||
const info = this.excitablesMap.get(excitable);
|
||
if (!info) return false;
|
||
if (callEnd) {
|
||
info.end?.();
|
||
}
|
||
this.excitables.delete(info.id);
|
||
this.excitablesMap.delete(info.obj);
|
||
return true;
|
||
}
|
||
|
||
hasExcitable(id: number): boolean {
|
||
return this.excitables.has(id);
|
||
}
|
||
|
||
//#endregion
|
||
|
||
//#region 元素销毁
|
||
|
||
destroy() {
|
||
super.destroy();
|
||
this.excitation.destroy();
|
||
this.abort?.abort();
|
||
}
|
||
|
||
//#endregion
|
||
|
||
//#region 调试功能
|
||
|
||
private toTagString(
|
||
item: IRenderItem,
|
||
space: number,
|
||
deep: number
|
||
): string {
|
||
if (item.isComment) return '';
|
||
const name = item.constructor.name;
|
||
if (item.children.size === 0) {
|
||
return `${' '.repeat(deep * space)}<${name} ${
|
||
item.id ? `id="${item.id}" ` : ''
|
||
}uid="${item.uid}"${item.hidden ? ' hidden' : ''} />\n`;
|
||
} else {
|
||
return (
|
||
`${' '.repeat(deep * space)}<${name} ${
|
||
item.id ? `${item.id} ` : ''
|
||
}uid="${item.uid}" ${item.hidden ? 'hidden' : ''}>\n` +
|
||
`${[...item.children]
|
||
.filter(v => !v.isComment)
|
||
.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);
|
||
}
|
||
|
||
//#endregion
|
||
}
|