template/packages/render/src/core/render.ts
2026-03-16 12:23:02 +08:00

898 lines
27 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}