refactor: enter 和 leave 事件 & patchProp 实现

This commit is contained in:
unanmed 2025-02-24 11:37:05 +08:00
parent 73acde7f49
commit 91e448fdcc
17 changed files with 467 additions and 345 deletions

View File

@ -1,4 +1,3 @@
import { ElementNamespace, ComponentInternalInstance } from 'vue';
import { MotaOffscreenCanvas2D } from '../fx/canvas2d';
import { ActionType, EventProgress, ActionEventMap } from './event';
import {
@ -168,20 +167,18 @@ export class ContainerCustom extends Container {
this.renderFn = render;
}
patchProp(
protected handleProps(
key: string,
prevValue: any,
nextValue: any,
namespace?: ElementNamespace,
parentComponent?: ComponentInternalInstance | null
): void {
nextValue: any
): boolean {
switch (key) {
case 'render': {
if (!this.assertType(nextValue, 'function', key)) return;
if (!this.assertType(nextValue, 'function', key)) return false;
this.setRenderFn(nextValue);
return;
return true;
}
}
super.patchProp(key, prevValue, nextValue, namespace, parentComponent);
return super.handleProps(key, prevValue, nextValue);
}
}

View File

@ -49,21 +49,11 @@ export const enum EventProgress {
Bubble
}
export interface IActionEvent {
export interface IActionEventBase {
/** 当前事件是监听的哪个元素 */
target: RenderItem;
/** 这次操作的标识符,在按下、移动、抬起阶段中保持不变 */
identifier: number;
/** 是触摸操作还是鼠标操作 */
touch: boolean;
/** 相对于触发元素左上角的横坐标 */
offsetX: number;
/** 相对于触发元素左上角的纵坐标 */
offsetY: number;
/** 相对于整个画布左上角的横坐标 */
absoluteX: number;
/** 相对于整个画布左上角的纵坐标 */
absoluteY: number;
/**
* {@link MouseType.None}
* {@link MouseType}
@ -83,6 +73,19 @@ export interface IActionEvent {
ctrlKey: boolean;
/** 触发时是否按下了 Windows(Windows) / Command(Mac) 键 */
metaKey: boolean;
}
export interface IActionEvent extends IActionEventBase {
/** 这次操作的标识符,在按下、移动、抬起阶段中保持不变 */
identifier: number;
/** 相对于触发元素左上角的横坐标 */
offsetX: number;
/** 相对于触发元素左上角的纵坐标 */
offsetY: number;
/** 相对于整个画布左上角的横坐标 */
absoluteX: number;
/** 相对于整个画布左上角的纵坐标 */
absoluteY: number;
/**
*
@ -120,14 +123,10 @@ export interface ERenderItemActionEvent {
upCapture: [ev: Readonly<IActionEvent>];
/** 当鼠标或手指在该元素上抬起的冒泡阶段触发 */
up: [ev: Readonly<IActionEvent>];
/** 当鼠标或手指进入该元素的捕获阶段触发 */
enterCapture: [ev: Readonly<IActionEvent>];
/** 当鼠标或手指进入该元素的冒泡阶段触发 */
enter: [ev: Readonly<IActionEvent>];
/** 当鼠标或手指离开该元素的捕获阶段触发 */
leaveCapture: [ev: Readonly<IActionEvent>];
/** 当鼠标或手指离开该元素的冒泡阶段触发 */
leave: [ev: Readonly<IActionEvent>];
/** 当鼠标或手指进入该元素时触发 */
enter: [ev: Readonly<IActionEventBase>];
/** 当鼠标或手指离开该元素时触发 */
leave: [ev: Readonly<IActionEventBase>];
/** 当鼠标滚轮时的捕获阶段触发 */
wheelCapture: [ev: Readonly<IWheelEvent>];
/** 当鼠标滚轮时的冒泡阶段触发 */
@ -144,7 +143,7 @@ export interface ActionEventMap {
[ActionType.Wheel]: IWheelEvent;
}
export const eventNameMap: Record<ActionType, string> = {
export const eventNameMap: Record<ActionType, keyof ERenderItemActionEvent> = {
[ActionType.Click]: 'click',
[ActionType.Down]: 'down',
[ActionType.Move]: 'move',

View File

@ -272,7 +272,9 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
alpha: number = 1;
/** 鼠标覆盖在此元素上时的光标样式 */
cursor: string = 'auto';
cursor: string = 'inherit';
/** 该元素是否忽略交互事件 */
noEvent: boolean = false;
get x() {
return this._transform.x;
@ -340,14 +342,12 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
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();
readonly touchId: Set<number> = new Set();
//#endregion
@ -650,7 +650,7 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
//#region 父子关系
checkRoot() {
checkRoot(): RenderItem | null {
if (this._root) return this._root;
if (this.isRoot) return this;
let ele: RenderItem = this;
@ -756,10 +756,12 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
type: ActionType,
progress: EventProgress
): keyof ERenderItemActionEvent {
if (progress === EventProgress.Capture) {
if (type === ActionType.Enter || type === ActionType.Leave) {
return eventNameMap[type];
} else if (progress === EventProgress.Capture) {
return `${eventNameMap[type]}Capture` as keyof ERenderItemActionEvent;
} else {
return eventNameMap[type] as keyof ERenderItemActionEvent;
return eventNameMap[type];
}
}
@ -829,6 +831,7 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
progress: EventProgress,
event: ActionEventMap[T]
): ActionEventMap[T] | null {
if (this.noEvent) return null;
if (progress === EventProgress.Capture) {
// 捕获阶段需要计算鼠标位置
const tran = this.transformFallThrough
@ -836,7 +839,6 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
: this._transform;
if (!tran) return null;
const [nx, ny] = this.calActionPosition(event, tran);
const inElement = this.isActionInElement(nx, ny);
// 在元素范围内,执行事件
const newEvent: ActionEventMap[T] = {
@ -877,17 +879,6 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
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: {
@ -929,10 +920,15 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
* @returns
*/
protected processBubble<T extends ActionType>(
_type: T,
type: T,
_event: ActionEventMap[T],
inElement: boolean
): boolean {
switch (type) {
case ActionType.Enter:
case ActionType.Leave:
return false;
}
return inElement;
}
@ -1039,6 +1035,21 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
return false;
}
/**
* props override props
* @param key props
* @param prevValue props
* @param nextValue props
* @returns
*/
protected handleProps(
_key: string,
_prevValue: any,
_nextValue: any
): boolean {
return false;
}
patchProp(
key: string,
prevValue: any,
@ -1047,6 +1058,7 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
_parentComponent?: ComponentInternalInstance | null
): void {
if (isNil(prevValue) && isNil(nextValue)) return;
if (this.handleProps(key, prevValue, nextValue)) return;
switch (key) {
case 'x': {
if (!this.assertType(nextValue, 'number', key)) return;
@ -1093,11 +1105,16 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
this.setHD(nextValue);
return;
}
case 'antiAliasing': {
case 'anti': {
if (!this.assertType(nextValue, 'boolean', key)) return;
this.setAntiAliasing(nextValue);
return;
}
case 'noanti': {
if (!this.assertType(nextValue, 'boolean', key)) return;
this.setAntiAliasing(!nextValue);
return;
}
case 'hidden': {
if (!this.assertType(nextValue, 'boolean', key)) return;
if (nextValue) this.hide();

View File

@ -18,7 +18,6 @@ import { getDamageColor } from '@/plugin/utils';
import { ERenderItemEvent, RenderItem } from '../item';
import EventEmitter from 'eventemitter3';
import { Transform } from '../transform';
import { ElementNamespace, ComponentInternalInstance } from 'vue';
import { transformCanvas } from '../utils';
const ensureFloorDamage = Mota.require('fn', 'ensureFloorDamage');
@ -529,46 +528,44 @@ export class Damage extends RenderItem<EDamageEvent> {
// console.timeEnd('damage');
}
patchProp(
protected handleProps(
key: string,
prevValue: any,
nextValue: any,
namespace?: ElementNamespace,
parentComponent?: ComponentInternalInstance | null
): void {
_prevValue: any,
nextValue: any
): boolean {
switch (key) {
case 'mapWidth':
if (!this.assertType(nextValue, 'number', key)) return;
if (!this.assertType(nextValue, 'number', key)) return false;
this.setMapSize(nextValue, this.mapHeight);
return;
return true;
case 'mapHeight':
if (!this.assertType(nextValue, 'number', key)) return;
if (!this.assertType(nextValue, 'number', key)) return false;
this.setMapSize(this.mapWidth, nextValue);
return;
return true;
case 'cellSize':
if (!this.assertType(nextValue, 'number', key)) return;
if (!this.assertType(nextValue, 'number', key)) return false;
this.setCellSize(nextValue);
return;
return true;
case 'enemy':
if (!this.assertType(nextValue, 'object', key)) return;
if (!this.assertType(nextValue, 'object', key)) return false;
this.updateCollection(nextValue);
return;
return true;
case 'font':
if (!this.assertType(nextValue, 'string', key)) return;
if (!this.assertType(nextValue, 'string', key)) return false;
this.font = nextValue;
this.update();
return;
return true;
case 'strokeStyle':
this.strokeStyle = nextValue;
this.update();
return;
return true;
case 'strokeWidth':
if (!this.assertType(nextValue, 'number', key)) return;
if (!this.assertType(nextValue, 'number', key)) return false;
this.strokeWidth = nextValue;
this.update();
return;
return true;
}
super.patchProp(key, prevValue, nextValue, namespace, parentComponent);
return false;
}
destroy(): void {

View File

@ -1,7 +1,6 @@
import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
import { ERenderItemEvent, RenderItem } from '../item';
import { Transform } from '../transform';
import { ElementNamespace, ComponentInternalInstance } from 'vue';
import { clamp, isNil } from 'lodash-es';
import { logger } from '@/core/common/logger';
@ -167,15 +166,22 @@ export abstract class GraphicItemBase
}
const path = this.cachePath;
if (!path) return false;
const fixX = x * devicePixelRatio;
const fixY = y * devicePixelRatio;
ctx.lineWidth = this.lineWidth;
ctx.lineCap = this.lineCap;
ctx.lineJoin = this.lineJoin;
ctx.setLineDash(this.lineDash);
switch (this.mode) {
case GraphicMode.Fill:
return ctx.isPointInPath(path, x, y, this.fillRule);
return ctx.isPointInPath(path, fixX, fixY, this.fillRule);
case GraphicMode.Stroke:
return ctx.isPointInStroke(path, fixX, fixY);
case GraphicMode.FillAndStroke:
case GraphicMode.StrokeAndFill:
return (
ctx.isPointInPath(path, x, y, this.fillRule) ||
ctx.isPointInStroke(path, x, y)
ctx.isPointInPath(path, fixX, fixY, this.fillRule) ||
ctx.isPointInStroke(path, fixX, fixY)
);
}
}
@ -287,69 +293,66 @@ export abstract class GraphicItemBase
ctx.miterLimit = this.miterLimit;
}
patchProp(
protected handleProps(
key: string,
prevValue: any,
nextValue: any,
namespace?: ElementNamespace,
parentComponent?: ComponentInternalInstance | null
): void {
if (isNil(prevValue) && isNil(nextValue)) return;
_prevValue: any,
nextValue: any
): boolean {
switch (key) {
case 'fill':
if (!this.assertType(nextValue, 'boolean', key)) return;
if (!this.assertType(nextValue, 'boolean', key)) return false;
this.checkMode(GraphicModeProp.Fill, nextValue);
break;
return true;
case 'stroke':
if (!this.assertType(nextValue, 'boolean', key)) return;
if (!this.assertType(nextValue, 'boolean', key)) return false;
this.checkMode(GraphicModeProp.Stroke, nextValue);
break;
return true;
case 'strokeAndFill':
if (!this.assertType(nextValue, 'number', key)) return;
if (!this.assertType(nextValue, 'number', key)) return false;
this.checkMode(GraphicModeProp.StrokeAndFill, nextValue);
break;
return true;
case 'fillRule':
if (!this.assertType(nextValue, 'string', key)) return;
if (!this.assertType(nextValue, 'string', key)) return false;
this.setFillRule(nextValue);
break;
return true;
case 'fillStyle':
this.setFillStyle(nextValue);
break;
return true;
case 'strokeStyle':
this.setStrokeStyle(nextValue);
break;
return true;
case 'lineWidth':
if (!this.assertType(nextValue, 'number', key)) return;
if (!this.assertType(nextValue, 'number', key)) return false;
this.lineWidth = nextValue;
this.update();
break;
return true;
case 'lineDash':
if (!this.assertType(nextValue, Array, key)) return;
if (!this.assertType(nextValue, Array, key)) return false;
this.lineDash = nextValue as number[];
this.update();
break;
return true;
case 'lineDashOffset':
if (!this.assertType(nextValue, 'number', key)) return;
if (!this.assertType(nextValue, 'number', key)) return false;
this.lineDashOffset = nextValue;
this.update();
break;
return true;
case 'lineJoin':
if (!this.assertType(nextValue, 'string', key)) return;
if (!this.assertType(nextValue, 'string', key)) return false;
this.lineJoin = nextValue;
this.update();
break;
return true;
case 'lineCap':
if (!this.assertType(nextValue, 'string', key)) return;
if (!this.assertType(nextValue, 'string', key)) return false;
this.lineCap = nextValue;
this.update();
break;
return true;
case 'miterLimit':
if (!this.assertType(nextValue, 'number', key)) return;
if (!this.assertType(nextValue, 'number', key)) return false;
this.miterLimit = nextValue;
this.update();
break;
return true;
}
super.patchProp(key, prevValue, nextValue, namespace, parentComponent);
return false;
}
}
@ -407,29 +410,27 @@ export class Circle extends GraphicItemBase {
this.update();
}
patchProp(
protected handleProps(
key: string,
prevValue: any,
nextValue: any,
namespace?: ElementNamespace,
parentComponent?: ComponentInternalInstance | null
): void {
nextValue: any
): boolean {
switch (key) {
case 'radius':
if (!this.assertType(nextValue, 'number', key)) return;
if (!this.assertType(nextValue, 'number', key)) return false;
this.setRadius(nextValue);
return;
return true;
case 'start':
if (!this.assertType(nextValue, 'number', key)) return;
if (!this.assertType(nextValue, 'number', key)) return false;
this.setAngle(nextValue, this.end);
return;
return true;
case 'end':
if (!this.assertType(nextValue, 'number', key)) return;
if (!this.assertType(nextValue, 'number', key)) return false;
this.setAngle(this.start, nextValue);
return;
return true;
case 'circle': {
const value = nextValue as CircleParams;
if (!this.assertType(value, Array, key)) return;
if (!this.assertType(value, Array, key)) return false;
const [cx, cy, radius, start, end] = value;
if (!isNil(cx) && !isNil(cy)) {
this.pos(cx, cy);
@ -440,10 +441,10 @@ export class Circle extends GraphicItemBase {
if (!isNil(start) && !isNil(end)) {
this.setAngle(start, end);
}
return;
return true;
}
}
super.patchProp(key, prevValue, nextValue, namespace, parentComponent);
return super.handleProps(key, prevValue, nextValue);
}
}
@ -494,33 +495,31 @@ export class Ellipse extends GraphicItemBase {
this.update();
}
patchProp(
protected handleProps(
key: string,
prevValue: any,
nextValue: any,
namespace?: ElementNamespace,
parentComponent?: ComponentInternalInstance | null
): void {
nextValue: any
): boolean {
switch (key) {
case 'radiusX':
if (!this.assertType(nextValue, 'number', key)) return;
if (!this.assertType(nextValue, 'number', key)) return false;
this.setRadius(nextValue, this.radiusY);
return;
return true;
case 'radiusY':
if (!this.assertType(nextValue, 'number', key)) return;
if (!this.assertType(nextValue, 'number', key)) return false;
this.setRadius(this.radiusY, nextValue);
return;
return true;
case 'start':
if (!this.assertType(nextValue, 'number', key)) return;
if (!this.assertType(nextValue, 'number', key)) return false;
this.setAngle(nextValue, this.end);
return;
return true;
case 'end':
if (!this.assertType(nextValue, 'number', key)) return;
if (!this.assertType(nextValue, 'number', key)) return false;
this.setAngle(this.start, nextValue);
return;
return true;
case 'ellipse': {
const value = nextValue as EllipseParams;
if (!this.assertType(value, Array, key)) return;
if (!this.assertType(value, Array, key)) return false;
const [cx, cy, radiusX, radiusY, start, end] = value;
if (!isNil(cx) && !isNil(cy)) {
this.pos(cx, cy);
@ -531,10 +530,10 @@ export class Ellipse extends GraphicItemBase {
if (!isNil(start) && !isNil(end)) {
this.setAngle(start, end);
}
return;
return true;
}
}
super.patchProp(key, prevValue, nextValue, namespace, parentComponent);
return super.handleProps(key, prevValue, nextValue);
}
}
@ -584,37 +583,37 @@ export class Line extends GraphicItemBase {
this.pathDirty = true;
}
patchProp(
protected handleProps(
key: string,
prevValue: any,
nextValue: any,
namespace?: ElementNamespace,
parentComponent?: ComponentInternalInstance | null
): void {
nextValue: any
): boolean {
switch (key) {
case 'x1':
if (!this.assertType(nextValue, 'number', key)) return;
if (!this.assertType(nextValue, 'number', key)) return false;
this.setPoint1(nextValue, this.y1);
return;
return true;
case 'y1':
if (!this.assertType(nextValue, 'number', key)) return;
if (!this.assertType(nextValue, 'number', key)) return false;
this.setPoint1(this.x1, nextValue);
return;
return true;
case 'x2':
if (!this.assertType(nextValue, 'number', key)) return;
if (!this.assertType(nextValue, 'number', key)) return false;
this.setPoint2(nextValue, this.y2);
return;
return true;
case 'y2':
if (!this.assertType(nextValue, 'number', key)) return;
if (!this.assertType(nextValue, 'number', key)) return false;
this.setPoint2(this.x2, nextValue);
return;
return true;
case 'line':
if (!this.assertType(nextValue as number[], Array, key)) return;
if (!this.assertType(nextValue as number[], Array, key)) {
return false;
}
this.setPoint1(nextValue[0], nextValue[1]);
this.setPoint2(nextValue[2], nextValue[3]);
return;
return true;
}
super.patchProp(key, prevValue, nextValue, namespace, parentComponent);
return super.handleProps(key, prevValue, nextValue);
}
}
@ -685,6 +684,10 @@ export class BezierCurve extends GraphicItemBase {
this.update();
}
protected isActionInElement(x: number, y: number): boolean {
return x >= 0 && x < this.width && y >= 0 && y < this.height;
}
private fitRect() {
const left = Math.min(this.sx, this.cp1x, this.cp2x, this.ex);
const top = Math.min(this.sy, this.cp1y, this.cp2y, this.ey);
@ -695,55 +698,55 @@ export class BezierCurve extends GraphicItemBase {
this.pathDirty = true;
}
patchProp(
protected handleProps(
key: string,
prevValue: any,
nextValue: any,
namespace?: ElementNamespace,
parentComponent?: ComponentInternalInstance | null
): void {
nextValue: any
): boolean {
switch (key) {
case 'sx':
if (!this.assertType(nextValue, 'number', key)) return;
if (!this.assertType(nextValue, 'number', key)) return false;
this.setStart(nextValue, this.sy);
return;
return true;
case 'sy':
if (!this.assertType(nextValue, 'number', key)) return;
if (!this.assertType(nextValue, 'number', key)) return false;
this.setStart(this.sx, nextValue);
return;
return true;
case 'cp1x':
if (!this.assertType(nextValue, 'number', key)) return;
if (!this.assertType(nextValue, 'number', key)) return false;
this.setControl1(nextValue, this.cp1y);
return;
return true;
case 'cp1y':
if (!this.assertType(nextValue, 'number', key)) return;
if (!this.assertType(nextValue, 'number', key)) return false;
this.setControl1(this.cp1x, nextValue);
return;
return true;
case 'cp2x':
if (!this.assertType(nextValue, 'number', key)) return;
if (!this.assertType(nextValue, 'number', key)) return false;
this.setControl2(nextValue, this.cp2y);
return;
return true;
case 'cp2y':
if (!this.assertType(nextValue, 'number', key)) return;
if (!this.assertType(nextValue, 'number', key)) return false;
this.setControl2(this.cp2x, nextValue);
return;
return true;
case 'ex':
if (!this.assertType(nextValue, 'number', key)) return;
if (!this.assertType(nextValue, 'number', key)) return false;
this.setEnd(nextValue, this.ey);
return;
return true;
case 'ey':
if (!this.assertType(nextValue, 'number', key)) return;
if (!this.assertType(nextValue, 'number', key)) return false;
this.setEnd(this.ex, nextValue);
return;
return true;
case 'curve':
if (!this.assertType(nextValue as number[], Array, key)) return;
if (!this.assertType(nextValue as number[], Array, key)) {
return false;
}
this.setStart(nextValue[0], nextValue[1]);
this.setControl1(nextValue[2], nextValue[3]);
this.setControl2(nextValue[4], nextValue[5]);
this.setEnd(nextValue[6], nextValue[7]);
return;
return true;
}
super.patchProp(key, prevValue, nextValue, namespace, parentComponent);
return super.handleProps(key, prevValue, nextValue);
}
}
@ -822,46 +825,50 @@ export class QuadraticCurve extends GraphicItemBase {
this.pathDirty = true;
}
patchProp(
protected isActionInElement(x: number, y: number): boolean {
return x >= 0 && x < this.width && y >= 0 && y < this.height;
}
protected handleProps(
key: string,
prevValue: any,
nextValue: any,
namespace?: ElementNamespace,
parentComponent?: ComponentInternalInstance | null
): void {
nextValue: any
): boolean {
switch (key) {
case 'sx':
if (!this.assertType(nextValue, 'number', key)) return;
if (!this.assertType(nextValue, 'number', key)) return false;
this.setStart(nextValue, this.sy);
return;
return true;
case 'sy':
if (!this.assertType(nextValue, 'number', key)) return;
if (!this.assertType(nextValue, 'number', key)) return false;
this.setStart(this.sx, nextValue);
return;
return true;
case 'cpx':
if (!this.assertType(nextValue, 'number', key)) return;
if (!this.assertType(nextValue, 'number', key)) return false;
this.setControl(nextValue, this.cpy);
return;
return true;
case 'cpy':
if (!this.assertType(nextValue, 'number', key)) return;
if (!this.assertType(nextValue, 'number', key)) return false;
this.setControl(this.cpx, nextValue);
return;
return true;
case 'ex':
if (!this.assertType(nextValue, 'number', key)) return;
if (!this.assertType(nextValue, 'number', key)) return false;
this.setEnd(nextValue, this.ey);
return;
return true;
case 'ey':
if (!this.assertType(nextValue, 'number', key)) return;
if (!this.assertType(nextValue, 'number', key)) return false;
this.setEnd(this.ex, nextValue);
return;
return true;
case 'curve':
if (!this.assertType(nextValue as number[], Array, key)) return;
if (!this.assertType(nextValue as number[], Array, key)) {
return false;
}
this.setStart(nextValue[0], nextValue[1]);
this.setControl(nextValue[2], nextValue[3]);
this.setEnd(nextValue[4], nextValue[5]);
return;
return true;
}
super.patchProp(key, prevValue, nextValue, namespace, parentComponent);
return super.handleProps(key, prevValue, nextValue);
}
}
@ -886,22 +893,24 @@ export class Path extends GraphicItemBase {
this.update();
}
patchProp(
protected isActionInElement(x: number, y: number): boolean {
return x >= 0 && x < this.width && y >= 0 && y < this.height;
}
protected handleProps(
key: string,
prevValue: any,
nextValue: any,
namespace?: ElementNamespace,
parentComponent?: ComponentInternalInstance | null
): void {
nextValue: any
): boolean {
switch (key) {
case 'path':
if (!this.assertType(nextValue, Path2D, key)) return;
if (!this.assertType(nextValue, Path2D, key)) return false;
this.path = nextValue;
this.pathDirty = true;
this.update();
return;
return true;
}
super.patchProp(key, prevValue, nextValue, namespace, parentComponent);
return super.handleProps(key, prevValue, nextValue);
}
}
@ -1041,27 +1050,25 @@ export class RectR extends GraphicItemBase {
}
}
patchProp(
protected handleProps(
key: string,
prevValue: any,
nextValue: any,
namespace?: ElementNamespace,
parentComponent?: ComponentInternalInstance | null
): void {
nextValue: any
): boolean {
switch (key) {
case 'circle': {
const value = nextValue as RectRCircleParams;
if (!this.assertType(value, Array, key)) return;
if (!this.assertType(value, Array, key)) return false;
this.setCircle(value);
return;
return true;
}
case 'ellipse': {
const value = nextValue as RectREllipseParams;
if (!this.assertType(value, Array, key)) return;
if (!this.assertType(value, Array, key)) return false;
this.setEllipse(value);
return;
return true;
}
}
super.patchProp(key, prevValue, nextValue, namespace, parentComponent);
return super.handleProps(key, prevValue, nextValue);
}
}

View File

@ -9,7 +9,6 @@ import { BlockCacher, CanvasCacheItem, ICanvasCacheItem } from './block';
import { Transform } from '../transform';
import { LayerFloorBinder, LayerGroupFloorBinder } from './floor';
import { RenderAdapter } from '../adapter';
import { ElementNamespace, ComponentInternalInstance } from 'vue';
import { IAnimateFrame, renderEmits } from '../frame';
export interface ILayerGroupRenderExtends {
@ -332,36 +331,34 @@ export class LayerGroup
}
}
patchProp(
protected handleProps(
key: string,
prevValue: any,
nextValue: any,
namespace?: ElementNamespace,
parentComponent?: ComponentInternalInstance | null
): void {
_prevValue: any,
nextValue: any
): boolean {
switch (key) {
case 'cellSize':
if (!this.assertType(nextValue, 'number', key)) return;
if (!this.assertType(nextValue, 'number', key)) return false;
this.setCellSize(nextValue);
return;
return true;
case 'blockSize':
if (!this.assertType(nextValue, 'number', key)) return;
if (!this.assertType(nextValue, 'number', key)) return false;
this.setBlockSize(nextValue);
return;
return true;
case 'floorId': {
if (!this.assertType(nextValue, 'number', key)) return;
if (!this.assertType(nextValue, 'number', key)) return false;
const binder = this.getExtends('floor-binder');
if (binder instanceof LayerGroupFloorBinder) {
binder.bindFloor(nextValue);
}
return;
return true;
}
case 'camera':
if (!this.assertType(nextValue, Transform, key)) return;
if (!this.assertType(nextValue, Transform, key)) return false;
this.camera = nextValue;
return;
return true;
}
super.patchProp(key, prevValue, nextValue, namespace, parentComponent);
return false;
}
destroy(): void {
@ -1438,16 +1435,14 @@ export class Layer extends Container<ELayerEvent> {
});
}
patchProp(
protected handleProps(
key: string,
prevValue: any,
nextValue: any,
namespace?: ElementNamespace,
parentComponent?: ComponentInternalInstance | null
): void {
_prevValue: any,
nextValue: any
): boolean {
switch (key) {
case 'layer': {
if (!this.assertType(nextValue, 'string', key)) return;
if (!this.assertType(nextValue, 'string', key)) return false;
const parent = this.parent;
if (parent instanceof LayerGroup) {
parent.removeLayer(this);
@ -1457,30 +1452,30 @@ export class Layer extends Container<ELayerEvent> {
this.layer = nextValue;
}
this.update();
return;
return true;
}
case 'cellSize':
if (!this.assertType(nextValue, 'number', key)) return;
if (!this.assertType(nextValue, 'number', key)) return false;
this.setCellSize(nextValue);
return;
return true;
case 'mapWidth':
if (!this.assertType(nextValue, 'number', key)) return;
if (!this.assertType(nextValue, 'number', key)) return false;
this.setMapSize(nextValue, this.mapHeight);
return;
return true;
case 'mapHeight':
if (!this.assertType(nextValue, 'number', key)) return;
if (!this.assertType(nextValue, 'number', key)) return false;
this.setMapSize(this.mapWidth, nextValue);
return;
return true;
case 'background':
if (!this.assertType(nextValue, 'number', key)) return;
if (!this.assertType(nextValue, 'number', key)) return false;
this.setBackground(nextValue);
return;
return true;
case 'floorImage':
if (!this.assertType(nextValue, Array, key)) return;
if (!this.assertType(nextValue, Array, key)) return false;
this.setFloorImage(nextValue as FloorAnimate[]);
return;
return true;
}
super.patchProp(key, prevValue, nextValue, namespace, parentComponent);
return false;
}
private addToGroup(group: LayerGroup) {

View File

@ -1,7 +1,6 @@
import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
import { ERenderItemEvent, RenderItem, RenderItemPosition } from '../item';
import { Transform } from '../transform';
import { ElementNamespace, ComponentInternalInstance } from 'vue';
import { AutotileRenderable, RenderableData } from '../cache';
import { texture } from '../cache';
import { isNil } from 'lodash-es';
@ -114,33 +113,31 @@ export class Text extends RenderItem<ETextEvent> {
this.size(width, actualBoundingBoxAscent + actualBoundingBoxDescent);
}
patchProp(
protected handleProps(
key: string,
prevValue: any,
nextValue: any,
namespace?: ElementNamespace,
parentComponent?: ComponentInternalInstance | null
): void {
_prevValue: any,
nextValue: any
): boolean {
switch (key) {
case 'text':
if (!this.assertType(nextValue, 'string', key)) return;
if (!this.assertType(nextValue, 'string', key)) return false;
this.setText(nextValue);
return;
return true;
case 'fillStyle':
this.setStyle(nextValue, this.strokeStyle);
return;
return true;
case 'strokeStyle':
this.setStyle(this.fillStyle, nextValue);
return;
return true;
case 'font':
if (!this.assertType(nextValue, 'string', key)) return;
if (!this.assertType(nextValue, 'string', key)) return false;
this.setFont(nextValue);
break;
case 'strokeWidth':
this.setStrokeWidth(nextValue);
return;
return true;
}
super.patchProp(key, prevValue, nextValue, namespace, parentComponent);
return false;
}
}
@ -181,19 +178,17 @@ export class Image extends RenderItem<EImageEvent> {
this.update();
}
patchProp(
protected handleProps(
key: string,
prevValue: any,
nextValue: any,
namespace?: ElementNamespace,
parentComponent?: ComponentInternalInstance | null
): void {
_prevValue: any,
nextValue: any
): boolean {
switch (key) {
case 'image':
this.setImage(nextValue);
return;
return true;
}
super.patchProp(key, prevValue, nextValue, namespace, parentComponent);
return false;
}
}
@ -213,6 +208,10 @@ export class Comment extends RenderItem {
_canvas: MotaOffscreenCanvas2D,
_transform: Transform
): void {}
protected handleProps(): boolean {
return false;
}
}
export interface EIconEvent extends ERenderItemEvent {}
@ -313,31 +312,29 @@ export class Icon extends RenderItem<EIconEvent> implements IAnimateFrame {
super.destroy();
}
patchProp(
protected handleProps(
key: string,
prevValue: any,
nextValue: any,
namespace?: ElementNamespace,
parentComponent?: ComponentInternalInstance | null
): void {
_prevValue: any,
nextValue: any
): boolean {
switch (key) {
case 'icon':
this.setIcon(nextValue);
return;
return true;
case 'animate':
if (!this.assertType(nextValue, 'boolean', key)) return;
if (!this.assertType(nextValue, 'boolean', key)) return false;
this.animate = nextValue;
if (nextValue) renderEmits.addFramer(this);
else renderEmits.removeFramer(this);
this.update();
return;
return true;
case 'frame':
if (!this.assertType(nextValue, 'number', key)) return;
if (!this.assertType(nextValue, 'number', key)) return false;
this.frame = nextValue;
this.update();
return;
return true;
}
super.patchProp(key, prevValue, nextValue, namespace, parentComponent);
return false;
}
}
@ -544,23 +541,21 @@ export class Winskin extends RenderItem<EWinskinEvent> {
this.update();
}
patchProp(
protected handleProps(
key: string,
prevValue: any,
nextValue: any,
namespace?: ElementNamespace,
parentComponent?: ComponentInternalInstance | null
): void {
_prevValue: any,
nextValue: any
): boolean {
switch (key) {
case 'image':
if (!this.assertType(nextValue, 'string', key)) return;
if (!this.assertType(nextValue, 'string', key)) return false;
this.setImageByName(nextValue);
return;
return true;
case 'borderSize':
if (!this.assertType(nextValue, 'number', key)) return;
if (!this.assertType(nextValue, 'number', key)) return false;
this.setBorderSize(nextValue);
return;
return true;
}
super.patchProp(key, prevValue, nextValue, namespace, parentComponent);
return false;
}
}

View File

@ -2,9 +2,9 @@ import { logger } from '../common/logger';
import { MotaOffscreenCanvas2D } from '../fx/canvas2d';
import { Container } from './container';
import {
ActionEventMap,
ActionType,
IActionEvent,
IActionEventBase,
IWheelEvent,
MouseType,
WheelType
@ -49,6 +49,10 @@ export class MotaRenderer extends Container implements IRenderTreeRoot {
private abort?: AbortController;
/** 根据捕获行为判断光标样式 */
private targetCursor: string = 'auto';
/** 当前鼠标覆盖的元素 */
private hoveredElement: Set<RenderItem> = new Set();
/** 本次交互前鼠标覆盖的元素 */
private beforeHovered: Set<RenderItem> = new Set();
target!: MotaOffscreenCanvas2D;
@ -112,17 +116,26 @@ export class MotaRenderer extends Container implements IRenderTreeRoot {
this.lastMouse
);
this.targetCursor = 'auto';
const temp = this.beforeHovered;
temp.clear();
this.beforeHovered = this.hoveredElement;
this.hoveredElement = temp;
this.captureEvent(ActionType.Move, event);
});
canvas.addEventListener('mouseenter', ev => {
const event = this.createMouseAction(ev, ActionType.Enter);
this.emit('enterCapture', event);
this.emit('enter', 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 => {
const event = this.createMouseAction(ev, ActionType.Leave);
this.emit('leaveCapture', event);
this.emit('leave', event);
this.hoveredElement.forEach(v => {
v.emit('leave', this.createMouseActionBase(ev, v));
});
this.hoveredElement.clear();
this.beforeHovered.clear();
});
document.addEventListener('touchstart', ev => {
this.createTouchAction(ev, ActionType.Down).forEach(v => {
@ -133,27 +146,29 @@ export class MotaRenderer extends Container implements IRenderTreeRoot {
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 => {
this.createTouchAction(ev, ActionType.Up).forEach(v => {
this.captureEvent(ActionType.Up, v);
this.touchInfo.delete(v.identifier);
});
});
document.addEventListener('touchmove', ev => {
this.createTouchAction(ev, ActionType.Move).forEach(v => {
const touch = this.touchInfo.get(v.identifier);
if (!touch) return;
const inElement = this.isTouchInCanvas(v.offsetX, v.offsetY);
if (touch.hovered && !inElement) {
this.emit('leaveCapture', v);
this.emit('leave', v);
}
if (!touch.hovered && inElement) {
this.emit('enterCapture', v);
this.emit('enter', v);
}
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 => {
@ -246,6 +261,39 @@ export class MotaRenderer extends Container implements IRenderTreeRoot {
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,
@ -385,14 +433,40 @@ export class MotaRenderer extends Container implements IRenderTreeRoot {
return list;
}
bubbleEvent<T extends ActionType>(
type: T,
event: ActionEventMap[T]
): ActionEventMap[T] | null {
if (this.targetCursor !== this.target.canvas.style.cursor) {
this.target.canvas.style.cursor = this.targetCursor;
}
return super.bubbleEvent(type, event);
private checkMouseEnterLeave(
event: MouseEvent,
before: Set<RenderItem>,
now: Set<RenderItem>
) {
// 先 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<RenderItem>,
now: Set<RenderItem>
) {
// 先 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) {
@ -465,9 +539,10 @@ export class MotaRenderer extends Container implements IRenderTreeRoot {
}
hoverElement(element: RenderItem): void {
if (element.cursor !== 'auto') {
if (element.cursor !== 'inherit') {
this.targetCursor = element.cursor;
}
this.hoveredElement.add(element);
}
destroy() {

View File

@ -76,7 +76,7 @@ type _Define<P extends BaseProps, E extends ERenderItemEvent> = DefineComponent<
Readonly<P & MappingEvent<E>>
>;
type TagDefine<T extends object, E extends ERenderItemEvent> = T &
export type TagDefine<T extends object, E extends ERenderItemEvent> = T &
MappingEvent<E> &
ReservedProps;

View File

@ -8,6 +8,13 @@ import { ERenderItemEvent, RenderItem } from '../item';
import { tagMap } from './map';
import { logger } from '@/core/common/logger';
import { Comment, ETextEvent, Text } from '../preset/misc';
import { BaseProps } from './props';
import { TagDefine } from './elements';
export type DefaultProps<
P extends BaseProps = BaseProps,
E extends ERenderItemEvent = ERenderItemEvent
> = TagDefine<P, E>;
export const { createApp, render } = createRenderer<RenderItem, RenderItem>({
patchProp: function (

View File

@ -24,27 +24,47 @@ export interface CustomProps {
}
export interface BaseProps {
/** 元素的横坐标 */
x?: number;
/** 元素的纵坐标 */
y?: number;
/** 元素的横向锚点位置 */
anchorX?: number;
/** 元素的纵向锚点位置 */
anchorY?: number;
/** 元素的纵深,值越大越靠上 */
zIndex?: number;
/** 元素的宽度 */
width?: number;
/** 元素的高度 */
height?: number;
/** 元素的滤镜 */
filter?: string;
/** 是否启用高清画布 */
hd?: boolean;
antiAliasing?: boolean;
/** 是否启用抗锯齿 */
anti?: boolean;
/** 是否不启用抗锯齿,优先级大于 anti主要用于像素图片渲染 */
noanti?: boolean;
/** 元素是否隐藏,可以用于一些画面效果,也可以用于调试 */
hidden?: boolean;
/** 元素的变换矩阵 */
transform?: Transform;
/** 元素的定位模式static 表示常规定位absolute 定位模式下元素位置始终处于左上角 */
type?: RenderItemPosition;
/** 是否启用缓存,用处较少,主要用于一些默认不启用缓存的元素的特殊优化 */
cache?: boolean;
/** 是否不启用缓存,优先级大于 cache用处较少主要用于一些特殊优化 */
nocache?: boolean;
/** 是否启用变换矩阵下穿,下穿模式下,当前元素会使用由父元素传递过来的变换矩阵,而非元素自身的 */
fall?: boolean;
/** 这个元素的唯一标识符,不可重复 */
id?: string;
/** 这个元素的不透明度 */
alpha?: number;
/** 这个元素与已渲染内容的混合模式,默认为 source-over */
composite?: GlobalCompositeOperation;
/** 鼠标放在这个元素上时的光标样式 */
cursor?: string;
/**
* `[横坐标纵坐标宽度高度x锚点y锚点]`

View File

@ -6,7 +6,6 @@ import {
} from './item';
import { MotaOffscreenCanvas2D } from '../fx/canvas2d';
import { Transform } from './transform';
import { ElementNamespace, ComponentInternalInstance } from 'vue';
export interface ESpriteEvent extends ERenderItemEvent {}
@ -42,19 +41,17 @@ export class Sprite<
this.update(this);
}
patchProp(
protected handleProps(
key: string,
prevValue: any,
nextValue: any,
namespace?: ElementNamespace,
parentComponent?: ComponentInternalInstance | null
): void {
_prevValue: any,
nextValue: any
): boolean {
switch (key) {
case 'render':
if (!this.assertType(nextValue, 'function', key)) return;
if (!this.assertType(nextValue, 'function', key)) return false;
this.setRenderFn(nextValue);
break;
return true;
}
super.patchProp(key, prevValue, nextValue, namespace, parentComponent);
return false;
}
}

View File

@ -1,9 +1,9 @@
import { ElementLocator, Sprite } from '@/core/render';
import { DefaultProps, ElementLocator, Sprite } from '@/core/render';
import { defineComponent, ref, watch } from 'vue';
import { SetupComponentOptions } from './types';
import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
interface ProgressProps {
interface ProgressProps extends DefaultProps {
/** 进度条的位置 */
loc: ElementLocator;
/** 进度条的进度1表示完成0表示未完成 */

View File

@ -10,12 +10,12 @@ import {
} from 'vue';
import { SetupComponentOptions } from './types';
import { clamp } from 'lodash-es';
import { ElementLocator } from '@/core/render';
import { DefaultProps, ElementLocator } from '@/core/render';
/** 圆角矩形页码距离容器的边框大小,与 pageSize 相乘 */
const RECT_PAD = 0.1;
export interface PageProps {
export interface PageProps extends DefaultProps {
/** 共有多少页 */
pages: number;
/** 页码组件的定位 */
@ -40,6 +40,23 @@ const pageProps = {
props: ['pages', 'loc', 'pageSize']
} satisfies SetupComponentOptions<PageProps, {}, string, PageSlots>;
/**
* {@link PageProps} {@link PageExpose}
*
* ---
*
* page
* ```tsx
* <Page maxPage={5}>
* {
* (page: number) => {
* // 页码从第一页开始,因此这里索引要减一
* return items[page - 1].map(v => <text text={v.text} />)
* }
* }
* </Page>
* ```
*/
export const Page = defineComponent<PageProps, {}, string, PageSlots>(
(props, { slots, expose }) => {
const nowPage = ref(1);
@ -60,8 +77,8 @@ export const Page = defineComponent<PageProps, {}, string, PageSlots>(
const textLoc = ref<ElementLocator>([0, 0, 0, 0]);
// 两个监听的参数
const leftArrow = ref<Path2D>(new Path2D());
const rightArrow = ref<Path2D>(new Path2D());
const leftArrow = ref<Path2D>();
const rightArrow = ref<Path2D>();
const isFirst = computed(() => nowPage.value === 1);
const isLast = computed(() => nowPage.value === props.pages);

View File

@ -13,6 +13,7 @@ import {
import { SetupComponentOptions } from './types';
import {
Container,
DefaultProps,
ElementLocator,
RenderItem,
Sprite,
@ -38,7 +39,7 @@ export interface ScrollExpose {
scrollTo(y: number, time?: number): void;
}
export interface ScrollProps {
export interface ScrollProps extends DefaultProps {
loc: ElementLocator;
hor?: boolean;
noscroll?: boolean;

View File

@ -12,12 +12,12 @@ import {
watch
} from 'vue';
import { logger } from '@/core/common/logger';
import { Sprite } from '../../../core/render/sprite';
import { ContainerProps } from '../../../core/render/renderer';
import { Sprite } from '@/core/render/sprite';
import { ContainerProps, DefaultProps } from '@/core/render/renderer';
import { isNil } from 'lodash-es';
import { SetupComponentOptions } from './types';
import EventEmitter from 'eventemitter3';
import { Text } from '../../../core/render/preset';
import { Text } from '@/core/render/preset';
import {
ITextContentConfig,
TextContentTyper,
@ -26,7 +26,7 @@ import {
} from './textboxTyper';
export interface TextContentProps
extends ContainerProps,
extends DefaultProps,
Partial<ITextContentConfig> {
/** 显示的文字 */
text?: string;

View File

@ -1125,8 +1125,6 @@ export class TextContentParser {
this.checkRestLine(width, guess);
}
console.log(this.renderable);
return {
lineHeights: this.lineHeights,
data: this.renderable