import { logger } from '../common/logger'; import { MotaOffscreenCanvas2D } from '../fx/canvas2d'; import { Container } from './container'; import { ActionType, IActionEvent, IActionEventBase, IWheelEvent, MouseType, WheelType } from './event'; import { IRenderTreeRoot, RenderItem } from './item'; import { Transform } from './transform'; 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; /** 根据捕获行为判断光标样式 */ private targetCursor: string = 'auto'; /** 当前鼠标覆盖的元素 */ private hoveredElement: Set = new Set(); /** 本次交互前鼠标覆盖的元素 */ private beforeHovered: Set = new Set(); target!: MotaOffscreenCanvas2D; readonly isRoot = true; constructor(id: string = 'render-main') { super('static', false); const canvas = document.getElementById(id) as HTMLCanvasElement; if (!canvas) { logger.error(19); return; } this.target = new MotaOffscreenCanvas2D(true, canvas); this.size(core._PX_, core._PY_); this.target.withGameScale(true); this.target.setAntiAliasing(false); this.setAnchor(0.5, 0.5); this.transform.translate(240, 240); MotaRenderer.list.set(id, this); const update = () => { this.requestRenderFrame(() => { this.refresh(); update(); }); }; update(); this.listen(); } size(width: number, height: number): void { super.size(width, height); this.target.size(width, height); this.transform.setTranslate(width / 2, height / 2); } private listen() { // 画布监听 const canvas = this.target.canvas; canvas.addEventListener('mousedown', ev => { ev.preventDefault(); const mouse = this.getMouseType(ev); this.lastMouse = mouse; this.captureEvent( ActionType.Down, this.createMouseAction(ev, ActionType.Down, mouse) ); }); canvas.addEventListener('mouseup', ev => { ev.preventDefault(); const event = this.createMouseAction(ev, ActionType.Up); this.captureEvent(ActionType.Up, event); this.captureEvent(ActionType.Click, event); }); canvas.addEventListener('mousemove', ev => { ev.preventDefault(); 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, this.beforeHovered, this.hoveredElement ); }); canvas.addEventListener('mouseleave', ev => { ev.preventDefault(); this.hoveredElement.forEach(v => { v.emit('leave', this.createMouseActionBase(ev, v)); }); this.hoveredElement.clear(); this.beforeHovered.clear(); }); document.addEventListener('touchstart', ev => { ev.preventDefault(); this.createTouchAction(ev, ActionType.Down).forEach(v => { this.captureEvent(ActionType.Down, v); }); }); document.addEventListener('touchend', ev => { ev.preventDefault(); this.createTouchAction(ev, ActionType.Up).forEach(v => { this.captureEvent(ActionType.Up, v); this.captureEvent(ActionType.Click, v); this.touchInfo.delete(v.identifier); }); }); document.addEventListener('touchcancel', ev => { ev.preventDefault(); this.createTouchAction(ev, ActionType.Up).forEach(v => { this.captureEvent(ActionType.Up, v); this.touchInfo.delete(v.identifier); }); }); document.addEventListener('touchmove', ev => { ev.preventDefault(); this.createTouchAction(ev, ActionType.Move).forEach(v => { const touch = this.touchInfo.get(v.identifier); if (!touch) return; const temp = this.beforeHovered; temp.clear(); this.beforeHovered = this.hoveredElement; this.hoveredElement = temp; this.captureEvent(ActionType.Move, v); this.checkTouchEnterLeave( ev, this.beforeHovered, this.hoveredElement ); }); }); canvas.addEventListener('wheel', ev => { ev.preventDefault(); 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: { 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, target: RenderItem = this, mouse: MouseType = this.getMouseType(event) ): IActionEventBase { return { 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, target: RenderItem ): IActionEventBase { return { 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 / core.domStyle.scale; const y = event.offsetY / core.domStyle.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) / core.domStyle.scale; const y = (touch.clientY - rect.top) / core.domStyle.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, before: Set, now: Set ) { // 先 leave,再 enter before.forEach(v => { if (!now.has(v)) { v.emit('leave', this.createMouseActionBase(event, v)); } }); now.forEach(v => { if (!before.has(v)) { v.emit('enter', this.createMouseActionBase(event, v)); } }); } private checkTouchEnterLeave( event: TouchEvent, before: Set, now: Set ) { // 先 leave,再 enter before.forEach(v => { if (!now.has(v)) { v.emit('leave', this.createTouchActionBase(event, v)); } }); now.forEach(v => { if (!before.has(v)) { v.emit('enter', this.createTouchActionBase(event, v)); } }); } update(_item: RenderItem = this) { this.cacheDirty = true; } protected refresh(): void { if (!this.cacheDirty) return; this.target.clear(); this.renderContent(this.target, Transform.identity); } /** * 根据渲染元素的id获取一个渲染元素 * @param id 要获取的渲染元素id * @returns */ getElementById(id: string): RenderItem | 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: RenderItem, id: string): RenderItem | 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: RenderItem): 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: 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; } hoverElement(element: RenderItem): void { if (element.cursor !== 'inherit') { this.targetCursor = element.cursor; } this.hoveredElement.add(element); } 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}"${item.hidden ? ' hidden' : ''}>\n`; } else { return ( `${' '.repeat(deep * space)}<${name} id="${item.id}" type="${item.type}" ${item.hidden ? 'hidden' : ''}>\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) { return this.list.get(id); } } window.addEventListener('resize', () => { MotaRenderer.list.forEach(v => v.requestAfterFrame(() => v.refreshAllChildren()) ); }); // @ts-expect-error debug window.logTagTree = () => { console.log(MotaRenderer.get('render-main')?.toTagTree()); };