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 { /** 委托内容的 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 { /** 所有连接到此根元素的渲染元素的 id 到元素自身的映射 */ protected readonly 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(); /** 渲染至的目标画布 */ readonly target!: MotaOffscreenCanvas2D; /** 当前元素是根元素 */ readonly isRoot = true; /** 当前元素的激励源 */ readonly excitation!: IExcitation; /** 下一帧之前需要执行的内容 */ private readonly beforeFrame: Set<() => void> = new Set(); /** 下一帧之后需要执行的内容 */ private readonly afterFrame: Set<() => void> = new Set(); /** 委托 excitables 的计数器 */ private delegationCounter: number = 0; /** 委托执行的 excitables */ private readonly excitables: Map = new Map(); /** 委托执行的 excitables 到其 id 的映射 */ private readonly excitablesMap: Map< ExcitableDelegation, DelegatedExcitable > = new Map(); /** 执行完毕需要删除的 excitables */ private readonly toDeleteExcitables: Set = new Set(); /** 标签注册信息 */ private readonly tagRegistry: Map = 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( tag: K, ...params: IRenderItemParameterMap[K] ): IRenderItemInstanceMap[K]; createElement

( 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, now: Set ) { // 先 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, now: Set ) { // 先 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)}\n` ); } } /** * 调试功能,将渲染树输出为 XML 标签形式,只包含渲染元素类名,以及元素 id 等基础属性,不包含属性值等 * @param space 缩进空格数 */ toTagTree(space: number = 4) { if (!import.meta.env.DEV) return ''; return this.toTagString(this, space, 0); } //#endregion }