HumanBreak/src/core/render/item.ts

1142 lines
34 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 { isNil } from 'lodash-es';
import { EventEmitter } from 'eventemitter3';
import { MotaOffscreenCanvas2D } from '../fx/canvas2d';
import { Ticker, TickerFn } from 'mutate-animate';
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,
transform: Transform
) => void;
export type RenderItemPosition = 'absolute' | 'static';
export interface IRenderUpdater {
/**
* 更新这个渲染元素
* @param item 触发更新事件的元素,不填默认为元素自身触发
*/
update(item?: RenderItem): void;
}
export interface IRenderAnchor {
/** 锚点横坐标0表示最左端1表示最右端 */
anchorX: number;
/** 锚点纵坐标0表示最上端1表示最下端 */
anchorY: number;
/**
* 设置渲染元素的位置锚点
* @param x 锚点的横坐标小数0表示最左边1表示最右边
* @param y 锚点的纵坐标小数0表示最上边1表示最下边
*/
setAnchor(x: number, y: number): void;
}
export interface IRenderConfig {
/** 是否是高清画布 */
highResolution: boolean;
/** 是否启用抗锯齿 */
antiAliasing: boolean;
/**
* 设置当前渲染元素是否使用高清画布
* @param hd 是否高清
*/
setHD(hd: boolean): void;
/**
* 设置当前渲染元素是否启用抗锯齿
* @param anti 是否抗锯齿
*/
setAntiAliasing(anti: boolean): void;
}
export interface IRenderChildable {
/** 当前元素的子元素 */
children: Set<RenderItem>;
/**
* 向这个元素添加子元素
* @param child 添加的元素
*/
appendChild(...child: RenderItem<any>[]): void;
/**
* 移除这个元素中的某个子元素
* @param child 要移除的元素
*/
removeChild(...child: RenderItem<any>[]): void;
/**
* 在下一个tick的渲染前对子元素进行排序
*/
requestSort(): void;
}
export interface IRenderFrame {
/**
* 在下一帧渲染之前执行函数,常用于渲染前数据更新,理论上不应当用于渲染,不保证运行顺序
* @param fn 执行的函数
*/
requestBeforeFrame(fn: () => void): void;
/**
* 在下一帧渲染之后执行函数,理论上不应当用于渲染,不保证运行顺序
* @param fn 执行的函数
*/
requestAfterFrame(fn: () => void): void;
/**
* 在下一帧渲染时执行函数,理论上应当只用于渲染(即{@link RenderItem.update}方法),且不保证运行顺序
* @param fn 执行的函数
*/
requestRenderFrame(fn: () => void): void;
}
export interface IRenderTickerSupport {
/**
* 委托ticker让其在指定时间范围内每帧执行对应函数超过时间后自动删除
* @param fn 每帧执行的函数
* @param time 函数持续时间,不填代表不会自动删除,需要手动删除
* @param end 持续时间结束后执行的函数
* @returns 委托id可用于删除
*/
delegateTicker(fn: TickerFn, time?: number, end?: () => void): number;
/**
* 移除ticker函数
* @param id 函数id也就是{@link IRenderTickerSupport.delegateTicker}的返回值
* @param callEnd 是否调用结束函数,即{@link IRenderTickerSupport.delegateTicker}的end参数默认调用
* @returns 是否删除成功比如对应ticker不存在就是删除失败
*/
removeTicker(id: number, callEnd?: boolean): boolean;
/**
* 检查是否包含一个委托函数
* @param id 函数id
*/
hasTicker(id: number): boolean;
}
export interface IRenderVueSupport {
/**
* 在 jsx, vue 中当属性改变后触发此函数,用于处理响应式等情况
* @param key 属性键名
* @param prevValue 该属性先前的数值
* @param nextValue 该属性当前的数值
* @param namespace 元素命名空间
* @param parentComponent 元素的父组件
*/
patchProp(
key: string,
prevValue: any,
nextValue: any,
namespace?: ElementNamespace,
parentComponent?: ComponentInternalInstance | null
): void;
}
export interface IRenderTreeRoot {
readonly isRoot: true;
/**
* 将一个渲染元素连接到此根元素
* @param item 要连接到此根元素的渲染元素
*/
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;
/**
* 当鼠标覆盖在某个元素上时执行
* @param element 鼠标覆盖的元素
*/
hoverElement(element: RenderItem): void;
}
export interface ERenderItemEvent extends ERenderItemActionEvent {
beforeRender: [transform: Transform];
afterRender: [transform: Transform];
destroy: [];
}
interface TickerDelegation {
fn: TickerFn;
timeout?: number;
endFn?: () => void;
}
const beforeFrame: (() => void)[] = [];
const afterFrame: (() => void)[] = [];
const renderFrame: (() => void)[] = [];
let count = 0;
export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
extends EventEmitter<ERenderItemEvent | E>
implements
IRenderUpdater,
IRenderAnchor,
IRenderConfig,
IRenderFrame,
IRenderTickerSupport,
IRenderChildable,
IRenderVueSupport
{
/** 渲染的全局ticker */
static ticker: Ticker = new Ticker();
/** 包括但不限于怪物、npc、自动元件的动画帧数 */
static animatedFrame: number = 0;
/** ticker委托映射 */
static tickerMap: Map<number, TickerDelegation> = new Map();
/** ticker委托id */
static tickerId: number = 0;
readonly uid: number = count++;
//#region 元素属性
private _id: string = '';
/**
* 元素的 id原则上不可重复
*/
get id(): string {
return this._id;
}
set id(v: string) {
this.checkRoot();
const prev = this._id;
this._id = v;
this._root?.modifyId(this, prev, v);
}
/** 元素纵深,表示了遮挡关系 */
zIndex: number = 0;
width: number = 200;
height: number = 200;
/** 渲染锚点,(0,0)表示左上角,(1,1)表示右下角 */
anchorX: number = 0;
/** 渲染锚点,(0,0)表示左上角,(1,1)表示右下角 */
anchorY: number = 0;
/** 渲染模式absolute表示绝对位置static表示跟随摄像机移动 */
type: RenderItemPosition = 'static';
/** 是否是高清画布 */
highResolution: boolean = true;
/** 是否抗锯齿 */
antiAliasing: boolean = true;
/** 是否被隐藏 */
hidden: boolean = false;
/** 滤镜 */
filter: string = 'none';
/** 混合方式 */
composite: GlobalCompositeOperation = 'source-over';
/** 不透明度 */
alpha: number = 1;
/** 鼠标覆盖在此元素上时的光标样式 */
cursor: string = 'auto';
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;
}
//#endregion
//#region 父子关系
private _parent?: RenderItem;
/** 当前元素的父元素 */
get parent() {
return this._parent;
}
/** 当前元素是否为根元素,如果是根元素,那么必须实现 `IRenderTreeRoot` 接口 */
readonly isRoot: boolean = false;
private _root?: RenderItem & IRenderTreeRoot;
get root() {
return this._root;
}
/** 当前元素是否已经连接至任意根元素 */
get connected() {
return !!this._root;
}
/** 该渲染元素的子元素 */
children: Set<RenderItem<ERenderItemEvent>> = new Set();
//#endregion
//#region 渲染配置与缓存
/** 渲染缓存信息 */
protected cache: MotaOffscreenCanvas2D;
/** 是否需要更新缓存 */
protected cacheDirty: boolean = false;
/** 是否启用缓存机制 */
readonly enableCache: boolean = true;
/** 是否启用transform下穿机制即画布的变换是否会继续作用到下一层画布 */
readonly transformFallThrough: boolean = false;
/** 这个渲染元素使用到的所有画布 */
protected readonly canvases: Set<MotaOffscreenCanvas2D> = new Set();
//#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,
enableCache: boolean = true,
transformFallThrough: boolean = false
) {
super();
this.enableCache = enableCache;
this.transformFallThrough = transformFallThrough;
this.type = type;
this._transform.bind(this);
this.cache = this.requireCanvas();
this.cache.withGameScale(true);
}
/**
* 渲染函数
* @param canvas 渲染至的画布
* @param transform 当前变换矩阵的,渲染时已经进行变换处理,不需要对画布再次进行变换处理。
* 此参数可用于自己对元素进行变换处理,也会用于对子元素的处理。
* 例如对于`absolute`类型的元素,同时有对视角改变的需求,就可以通过此参数进行变换。
* 样板内置的`Layer`及`Damage`元素就是通过此方式实现的
*/
protected abstract render(
canvas: MotaOffscreenCanvas2D,
transform: Transform
): void;
/**
* 渲染当前对象
* @param canvas 渲染至的画布
* @param transform 由父元素传递过来的变换矩阵
*/
renderContent(canvas: MotaOffscreenCanvas2D, transform: Transform) {
if (this.hidden) return;
this.emit('beforeRender', 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;
const ctx = canvas.ctx;
ctx.save();
canvas.setAntiAliasing(this.antiAliasing);
if (this.type === 'static') transformCanvas(canvas, tran);
ctx.filter = this.filter;
ctx.globalAlpha = this.alpha;
ctx.globalCompositeOperation = this.composite;
if (this.enableCache) {
const { width, height, ctx } = this.cache;
if (this.cacheDirty) {
const { canvas } = this.cache;
ctx.clearRect(0, 0, canvas.width, canvas.height);
this.render(this.cache, tran);
this.cacheDirty = false;
}
canvas.ctx.drawImage(this.cache.canvas, ax, ay, width, height);
} else {
this.cacheDirty = false;
canvas.ctx.translate(ax, ay);
this.render(canvas, tran);
}
ctx.restore();
this.emit('afterRender', transform);
}
/**
* 申请一个 `MotaOffscreenCanvas2D`,即申请一个画布
* @param alpha 是否启用画布的 alpha 通道
*/
protected requireCanvas(alpha: boolean = true) {
const canvas = new MotaOffscreenCanvas2D(alpha);
this.canvases.add(canvas);
return canvas;
}
//#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.update();
}
/**
* 设置本元素的滤镜
* @param filter 滤镜
*/
setFilter(filter: string) {
this.filter = filter;
this.update(this);
}
/**
* 设置本元素渲染时的混合方式
* @param composite 混合方式
*/
setComposite(composite: GlobalCompositeOperation) {
this.composite = composite;
this.update();
}
/**
* 设置本元素的不透明度
* @param alpha 不透明度
*/
setAlpha(alpha: number) {
this.alpha = alpha;
this.update();
}
setHD(hd: boolean): void {
this.highResolution = hd;
this.cache.setHD(hd);
this.update(this);
}
setAntiAliasing(anti: boolean): void {
this.antiAliasing = anti;
this.cache.setAntiAliasing(anti);
this.update(this);
}
setZIndex(zIndex: number) {
this.zIndex = zIndex;
this.parent?.requestSort();
}
setAnchor(x: number, y: number): void {
this.anchorX = x;
this.anchorY = y;
this.update();
}
/**
* 隐藏这个元素
*/
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);
}
requestAfterFrame(fn: () => void): void {
afterFrame.push(fn);
}
requestRenderFrame(fn: () => void): void {
renderFrame.push(fn);
}
delegateTicker(fn: TickerFn, time?: number, end?: () => void): number {
const id = RenderItem.tickerId++;
if (typeof time === 'number' && time === 0) return id;
const delegation: TickerDelegation = {
fn,
endFn: end
};
RenderItem.tickerMap.set(id, delegation);
if (typeof time === 'number' && time < 2147438647 && time > 0) {
delegation.timeout = window.setTimeout(() => {
RenderItem.tickerMap.delete(id);
end?.();
}, time);
}
return id;
}
removeTicker(id: number, callEnd: boolean = true): boolean {
const delegation = RenderItem.tickerMap.get(id);
if (!delegation) return false;
RenderItem.ticker.remove(delegation.fn);
window.clearTimeout(delegation.timeout);
if (callEnd) delegation.endFn?.();
RenderItem.tickerMap.delete(id);
return true;
}
hasTicker(id: number): boolean {
return RenderItem.tickerMap.has(id);
}
//#endregion
//#region 父子关系
checkRoot() {
if (this._root) return this._root;
if (this.isRoot) return this;
let ele: RenderItem = this;
while (!ele.isRoot) {
if (ele._root) {
this._root = ele._root;
return this._root;
}
if (!ele._parent) {
return null;
} else {
ele = ele._parent;
}
}
this._root = ele as RenderItem & IRenderTreeRoot;
return ele;
}
/**
* 刷新所有子元素
*/
refreshAllChildren() {
if (this.children.size > 0) {
const stack: RenderItem[] = [this];
while (stack.length > 0) {
const item = stack.pop();
if (!item) continue;
item.cacheDirty = true;
item.children.forEach(v => stack.push(v));
}
}
this.update(this);
}
/**
* 将这个渲染元素添加到其他父元素上
* @param parent 父元素
*/
append(parent: RenderItem) {
this.remove();
parent.children.add(this);
this._parent = parent;
parent.requestSort();
this.update();
this.checkRoot();
this._root?.connect(this);
this.canvases.forEach(v => v.activate());
}
/**
* 从渲染树中移除这个节点
* @returns 是否移除成功
*/
remove(): boolean {
if (!this.parent) return false;
const parent = this.parent;
const success = parent.children.delete(this);
this._parent = void 0;
parent.requestSort();
parent.update();
this.canvases.forEach(v => v.deactivate());
if (!success) return false;
this._root?.disconnect(this);
this._root = void 0;
return true;
}
/**
* 添加子元素默认没有任何行为且会抛出警告你需要在自己的RenderItem继承类中复写它才可以使用
* @param child 子元素
*/
appendChild(..._child: RenderItem<any>[]): void {
logger.warn(35);
}
/**
* 移除子元素默认没有任何行为且会抛出警告你需要在自己的RenderItem继承类中复写它才可以使用
* @param child 子元素
*/
removeChild(..._child: RenderItem<any>[]): void {
logger.warn(36);
}
/**
* 申请对元素进行排序默认没有任何行为且会抛出警告你需要在自己的RenderItem继承类中复写它才可以使用
*/
requestSort(): void {
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 (inElement) {
this._root?.hoverElement(this);
}
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 (!inElement) return false;
if (event.touch) {
this.touchId.add(event.identifier);
} else {
this.mouseId.set(event.type, event.identifier);
}
break;
}
case ActionType.Click: {
if (!inElement) return false;
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;
}
}
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 {
const x = event.offsetX + this.anchorX * this.width;
const y = event.offsetY + this.anchorY * this.height;
if (this.type === 'absolute') return [x, y, 0];
else return transform.untransformed(x, y);
}
/**
* 判断一个点击事件是否在元素内,可以通过 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 实际值
* @param expected 期望类型
* @param key 键名
*/
protected assertType(value: any, expected: string, key: string): boolean;
/**
* 判断一个prop是否是期望类型
* @param value 实际值
* @param expected 期望类型
* @param key 键名
*/
protected assertType<T>(
value: any,
expected: new (...params: any[]) => T,
key: string
): value is T;
protected assertType(
value: any,
expected: string | (new (...params: any[]) => any),
key: string
) {
if (typeof expected === 'string') {
const type = typeof value;
if (type !== expected) {
logger.error(21, key, expected, type);
return false;
} else {
return true;
}
} else {
if (value instanceof expected) {
return true;
} else {
logger.error(
21,
key,
expected.name,
value?.constructor?.name ?? typeof value
);
return false;
}
}
}
/**
* 解析事件key
* @param key 键名
* @returns 返回字符串表示解析后的键名,返回布尔值表示不是事件
*/
protected parseEvent(key: string): string | false {
if (key.startsWith('on')) {
const code = key.charCodeAt(2);
if (code >= 65 && code <= 90) {
return key[2].toLowerCase() + key.slice(3);
}
}
return false;
}
patchProp(
key: string,
prevValue: any,
nextValue: any,
_namespace?: ElementNamespace,
_parentComponent?: ComponentInternalInstance | null
): void {
if (isNil(prevValue) && isNil(nextValue)) return;
switch (key) {
case 'x': {
if (!this.assertType(nextValue, 'number', key)) return;
this.pos(nextValue, this._transform.y);
return;
}
case 'y': {
if (!this.assertType(nextValue, 'number', key)) return;
this.pos(this._transform.x, nextValue);
return;
}
case 'anchorX': {
if (!this.assertType(nextValue, 'number', key)) return;
this.setAnchor(nextValue, this.anchorY);
return;
}
case 'anchorY': {
if (!this.assertType(nextValue, 'number', key)) return;
this.setAnchor(this.anchorX, nextValue);
return;
}
case 'zIndex': {
if (!this.assertType(nextValue, 'number', key)) return;
this.setZIndex(nextValue);
return;
}
case 'width': {
if (!this.assertType(nextValue, 'number', key)) return;
this.size(nextValue, this.height);
return;
}
case 'height': {
if (!this.assertType(nextValue, 'number', key)) return;
this.size(this.width, nextValue);
return;
}
case 'filter': {
if (!this.assertType(nextValue, 'string', key)) return;
this.setFilter(this.filter);
return;
}
case 'hd': {
if (!this.assertType(nextValue, 'boolean', key)) return;
this.setHD(nextValue);
return;
}
case 'antiAliasing': {
if (!this.assertType(nextValue, 'boolean', key)) return;
this.setAntiAliasing(nextValue);
return;
}
case 'hidden': {
if (!this.assertType(nextValue, 'boolean', key)) return;
if (nextValue) this.hide();
else this.show();
return;
}
case 'transform': {
if (!this.assertType(nextValue, Transform, key)) return;
this.transform = nextValue;
this.update();
return;
}
case 'type': {
if (!this.assertType(nextValue, 'string', key)) return;
this.type = nextValue;
this.update();
return;
}
case 'id': {
if (!this.assertType(nextValue, 'string', key)) return;
this.id = nextValue;
return;
}
case 'alpha': {
if (!this.assertType(nextValue, 'number', key)) return;
this.setAlpha(nextValue);
return;
}
case 'composite': {
if (!this.assertType(nextValue, 'string', key)) return;
this.setComposite(nextValue);
return;
}
case 'loc': {
if (!this.assertType(nextValue, Array, key)) return;
if (!isNil(nextValue[0]) && !isNil(nextValue[1])) {
this.pos(nextValue[0] as number, nextValue[1] as number);
}
if (!isNil(nextValue[2]) && !isNil(nextValue[3])) {
this.size(nextValue[2] as number, nextValue[3] as number);
}
if (!isNil(nextValue[4]) && !isNil(nextValue[5])) {
this.setAnchor(
nextValue[4] as number,
nextValue[5] as number
);
}
return;
}
case 'anc': {
if (!this.assertType(nextValue, Array, key)) return;
this.setAnchor(nextValue[0] as number, nextValue[1] as number);
return;
}
case 'cursor': {
if (!this.assertType(nextValue, 'string', key)) return;
this.cursor = nextValue;
return;
}
}
const ev = this.parseEvent(key);
if (ev) {
if (prevValue) {
this.off(ev as keyof ERenderItemEvent, prevValue);
}
this.on(ev as keyof ERenderItemEvent, nextValue);
}
}
//#endregion
/**
* 摧毁这个渲染元素,摧毁后不应继续使用
*/
destroy(): void {
this.remove();
this.emit('destroy');
this.removeAllListeners();
this.cache.delete();
}
}
RenderItem.ticker.add(time => {
// slice 是为了让函数里面的 request 进入下一帧执行
if (beforeFrame.length > 0) {
const arr = beforeFrame.slice();
beforeFrame.splice(0);
arr.forEach(v => v());
}
RenderItem.tickerMap.forEach(v => {
v.fn(time);
});
if (renderFrame.length > 0) {
const arr = renderFrame.slice();
renderFrame.splice(0);
arr.forEach(v => v());
}
if (afterFrame.length > 0) {
const arr = afterFrame.slice();
afterFrame.splice(0);
arr.forEach(v => v());
}
});