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 { MotaOffscreenCanvas2D } from '../fx/canvas2d';
import { ActionType, EventProgress, ActionEventMap } from './event'; import { ActionType, EventProgress, ActionEventMap } from './event';
import { import {
@ -168,20 +167,18 @@ export class ContainerCustom extends Container {
this.renderFn = render; this.renderFn = render;
} }
patchProp( protected handleProps(
key: string, key: string,
prevValue: any, prevValue: any,
nextValue: any, nextValue: any
namespace?: ElementNamespace, ): boolean {
parentComponent?: ComponentInternalInstance | null
): void {
switch (key) { switch (key) {
case 'render': { case 'render': {
if (!this.assertType(nextValue, 'function', key)) return; if (!this.assertType(nextValue, 'function', key)) return false;
this.setRenderFn(nextValue); 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 Bubble
} }
export interface IActionEvent { export interface IActionEventBase {
/** 当前事件是监听的哪个元素 */ /** 当前事件是监听的哪个元素 */
target: RenderItem; target: RenderItem;
/** 这次操作的标识符,在按下、移动、抬起阶段中保持不变 */
identifier: number;
/** 是触摸操作还是鼠标操作 */ /** 是触摸操作还是鼠标操作 */
touch: boolean; touch: boolean;
/** 相对于触发元素左上角的横坐标 */
offsetX: number;
/** 相对于触发元素左上角的纵坐标 */
offsetY: number;
/** 相对于整个画布左上角的横坐标 */
absoluteX: number;
/** 相对于整个画布左上角的纵坐标 */
absoluteY: number;
/** /**
* {@link MouseType.None} * {@link MouseType.None}
* {@link MouseType} * {@link MouseType}
@ -83,6 +73,19 @@ export interface IActionEvent {
ctrlKey: boolean; ctrlKey: boolean;
/** 触发时是否按下了 Windows(Windows) / Command(Mac) 键 */ /** 触发时是否按下了 Windows(Windows) / Command(Mac) 键 */
metaKey: boolean; 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>]; upCapture: [ev: Readonly<IActionEvent>];
/** 当鼠标或手指在该元素上抬起的冒泡阶段触发 */ /** 当鼠标或手指在该元素上抬起的冒泡阶段触发 */
up: [ev: Readonly<IActionEvent>]; up: [ev: Readonly<IActionEvent>];
/** 当鼠标或手指进入该元素的捕获阶段触发 */ /** 当鼠标或手指进入该元素时触发 */
enterCapture: [ev: Readonly<IActionEvent>]; enter: [ev: Readonly<IActionEventBase>];
/** 当鼠标或手指进入该元素的冒泡阶段触发 */ /** 当鼠标或手指离开该元素时触发 */
enter: [ev: Readonly<IActionEvent>]; leave: [ev: Readonly<IActionEventBase>];
/** 当鼠标或手指离开该元素的捕获阶段触发 */
leaveCapture: [ev: Readonly<IActionEvent>];
/** 当鼠标或手指离开该元素的冒泡阶段触发 */
leave: [ev: Readonly<IActionEvent>];
/** 当鼠标滚轮时的捕获阶段触发 */ /** 当鼠标滚轮时的捕获阶段触发 */
wheelCapture: [ev: Readonly<IWheelEvent>]; wheelCapture: [ev: Readonly<IWheelEvent>];
/** 当鼠标滚轮时的冒泡阶段触发 */ /** 当鼠标滚轮时的冒泡阶段触发 */
@ -144,7 +143,7 @@ export interface ActionEventMap {
[ActionType.Wheel]: IWheelEvent; [ActionType.Wheel]: IWheelEvent;
} }
export const eventNameMap: Record<ActionType, string> = { export const eventNameMap: Record<ActionType, keyof ERenderItemActionEvent> = {
[ActionType.Click]: 'click', [ActionType.Click]: 'click',
[ActionType.Down]: 'down', [ActionType.Down]: 'down',
[ActionType.Move]: 'move', [ActionType.Move]: 'move',

View File

@ -272,7 +272,9 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
alpha: number = 1; alpha: number = 1;
/** 鼠标覆盖在此元素上时的光标样式 */ /** 鼠标覆盖在此元素上时的光标样式 */
cursor: string = 'auto'; cursor: string = 'inherit';
/** 该元素是否忽略交互事件 */
noEvent: boolean = false;
get x() { get x() {
return this._transform.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 cachedEvent: Map<ActionType, IActionEvent> = new Map();
/** 下穿模式下当前下穿过来的变换矩阵 */ /** 下穿模式下当前下穿过来的变换矩阵 */
private fallTransform?: Transform; private fallTransform?: Transform;
/** 鼠标当前是否覆盖在当前元素上 */
private hovered: boolean = false;
/** 是否在元素内 */ /** 是否在元素内 */
private inElement: boolean = false; private inElement: boolean = false;
/** 鼠标标识符映射,键为按下的鼠标按键类型,值表示本次操作的唯一标识符,在按下、移动、抬起过程中保持一致 */ /** 鼠标标识符映射,键为按下的鼠标按键类型,值表示本次操作的唯一标识符,在按下、移动、抬起过程中保持一致 */
protected mouseId: Map<MouseType, number> = new Map(); protected mouseId: Map<MouseType, number> = new Map();
/** 当前所有的触摸标识符 */ /** 当前所有的触摸标识符 */
protected touchId: Set<number> = new Set(); readonly touchId: Set<number> = new Set();
//#endregion //#endregion
@ -650,7 +650,7 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
//#region 父子关系 //#region 父子关系
checkRoot() { checkRoot(): RenderItem | null {
if (this._root) return this._root; if (this._root) return this._root;
if (this.isRoot) return this; if (this.isRoot) return this;
let ele: RenderItem = this; let ele: RenderItem = this;
@ -756,10 +756,12 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
type: ActionType, type: ActionType,
progress: EventProgress progress: EventProgress
): keyof ERenderItemActionEvent { ): 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; return `${eventNameMap[type]}Capture` as keyof ERenderItemActionEvent;
} else { } else {
return eventNameMap[type] as keyof ERenderItemActionEvent; return eventNameMap[type];
} }
} }
@ -829,6 +831,7 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
progress: EventProgress, progress: EventProgress,
event: ActionEventMap[T] event: ActionEventMap[T]
): ActionEventMap[T] | null { ): ActionEventMap[T] | null {
if (this.noEvent) return null;
if (progress === EventProgress.Capture) { if (progress === EventProgress.Capture) {
// 捕获阶段需要计算鼠标位置 // 捕获阶段需要计算鼠标位置
const tran = this.transformFallThrough const tran = this.transformFallThrough
@ -836,7 +839,6 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
: this._transform; : this._transform;
if (!tran) return null; if (!tran) return null;
const [nx, ny] = this.calActionPosition(event, tran); const [nx, ny] = this.calActionPosition(event, tran);
const inElement = this.isActionInElement(nx, ny); const inElement = this.isActionInElement(nx, ny);
// 在元素范围内,执行事件 // 在元素范围内,执行事件
const newEvent: ActionEventMap[T] = { const newEvent: ActionEventMap[T] = {
@ -877,17 +879,6 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
if (inElement) { if (inElement) {
this._root?.hoverElement(this); 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; break;
} }
case ActionType.Down: { case ActionType.Down: {
@ -929,10 +920,15 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
* @returns * @returns
*/ */
protected processBubble<T extends ActionType>( protected processBubble<T extends ActionType>(
_type: T, type: T,
_event: ActionEventMap[T], _event: ActionEventMap[T],
inElement: boolean inElement: boolean
): boolean { ): boolean {
switch (type) {
case ActionType.Enter:
case ActionType.Leave:
return false;
}
return inElement; return inElement;
} }
@ -1039,6 +1035,21 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
return false; 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( patchProp(
key: string, key: string,
prevValue: any, prevValue: any,
@ -1047,6 +1058,7 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
_parentComponent?: ComponentInternalInstance | null _parentComponent?: ComponentInternalInstance | null
): void { ): void {
if (isNil(prevValue) && isNil(nextValue)) return; if (isNil(prevValue) && isNil(nextValue)) return;
if (this.handleProps(key, prevValue, nextValue)) return;
switch (key) { switch (key) {
case 'x': { case 'x': {
if (!this.assertType(nextValue, 'number', key)) return; if (!this.assertType(nextValue, 'number', key)) return;
@ -1093,11 +1105,16 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
this.setHD(nextValue); this.setHD(nextValue);
return; return;
} }
case 'antiAliasing': { case 'anti': {
if (!this.assertType(nextValue, 'boolean', key)) return; if (!this.assertType(nextValue, 'boolean', key)) return;
this.setAntiAliasing(nextValue); this.setAntiAliasing(nextValue);
return; return;
} }
case 'noanti': {
if (!this.assertType(nextValue, 'boolean', key)) return;
this.setAntiAliasing(!nextValue);
return;
}
case 'hidden': { case 'hidden': {
if (!this.assertType(nextValue, 'boolean', key)) return; if (!this.assertType(nextValue, 'boolean', key)) return;
if (nextValue) this.hide(); if (nextValue) this.hide();

View File

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

View File

@ -1,7 +1,6 @@
import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d'; import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
import { ERenderItemEvent, RenderItem } from '../item'; import { ERenderItemEvent, RenderItem } from '../item';
import { Transform } from '../transform'; import { Transform } from '../transform';
import { ElementNamespace, ComponentInternalInstance } from 'vue';
import { clamp, isNil } from 'lodash-es'; import { clamp, isNil } from 'lodash-es';
import { logger } from '@/core/common/logger'; import { logger } from '@/core/common/logger';
@ -167,15 +166,22 @@ export abstract class GraphicItemBase
} }
const path = this.cachePath; const path = this.cachePath;
if (!path) return false; 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) { switch (this.mode) {
case GraphicMode.Fill: case GraphicMode.Fill:
return ctx.isPointInPath(path, x, y, this.fillRule); return ctx.isPointInPath(path, fixX, fixY, this.fillRule);
case GraphicMode.Stroke: case GraphicMode.Stroke:
return ctx.isPointInStroke(path, fixX, fixY);
case GraphicMode.FillAndStroke: case GraphicMode.FillAndStroke:
case GraphicMode.StrokeAndFill: case GraphicMode.StrokeAndFill:
return ( return (
ctx.isPointInPath(path, x, y, this.fillRule) || ctx.isPointInPath(path, fixX, fixY, this.fillRule) ||
ctx.isPointInStroke(path, x, y) ctx.isPointInStroke(path, fixX, fixY)
); );
} }
} }
@ -287,69 +293,66 @@ export abstract class GraphicItemBase
ctx.miterLimit = this.miterLimit; ctx.miterLimit = this.miterLimit;
} }
patchProp( protected handleProps(
key: string, key: string,
prevValue: any, _prevValue: any,
nextValue: any, nextValue: any
namespace?: ElementNamespace, ): boolean {
parentComponent?: ComponentInternalInstance | null
): void {
if (isNil(prevValue) && isNil(nextValue)) return;
switch (key) { switch (key) {
case 'fill': case 'fill':
if (!this.assertType(nextValue, 'boolean', key)) return; if (!this.assertType(nextValue, 'boolean', key)) return false;
this.checkMode(GraphicModeProp.Fill, nextValue); this.checkMode(GraphicModeProp.Fill, nextValue);
break; return true;
case 'stroke': case 'stroke':
if (!this.assertType(nextValue, 'boolean', key)) return; if (!this.assertType(nextValue, 'boolean', key)) return false;
this.checkMode(GraphicModeProp.Stroke, nextValue); this.checkMode(GraphicModeProp.Stroke, nextValue);
break; return true;
case 'strokeAndFill': case 'strokeAndFill':
if (!this.assertType(nextValue, 'number', key)) return; if (!this.assertType(nextValue, 'number', key)) return false;
this.checkMode(GraphicModeProp.StrokeAndFill, nextValue); this.checkMode(GraphicModeProp.StrokeAndFill, nextValue);
break; return true;
case 'fillRule': case 'fillRule':
if (!this.assertType(nextValue, 'string', key)) return; if (!this.assertType(nextValue, 'string', key)) return false;
this.setFillRule(nextValue); this.setFillRule(nextValue);
break; return true;
case 'fillStyle': case 'fillStyle':
this.setFillStyle(nextValue); this.setFillStyle(nextValue);
break; return true;
case 'strokeStyle': case 'strokeStyle':
this.setStrokeStyle(nextValue); this.setStrokeStyle(nextValue);
break; return true;
case 'lineWidth': case 'lineWidth':
if (!this.assertType(nextValue, 'number', key)) return; if (!this.assertType(nextValue, 'number', key)) return false;
this.lineWidth = nextValue; this.lineWidth = nextValue;
this.update(); this.update();
break; return true;
case 'lineDash': case 'lineDash':
if (!this.assertType(nextValue, Array, key)) return; if (!this.assertType(nextValue, Array, key)) return false;
this.lineDash = nextValue as number[]; this.lineDash = nextValue as number[];
this.update(); this.update();
break; return true;
case 'lineDashOffset': case 'lineDashOffset':
if (!this.assertType(nextValue, 'number', key)) return; if (!this.assertType(nextValue, 'number', key)) return false;
this.lineDashOffset = nextValue; this.lineDashOffset = nextValue;
this.update(); this.update();
break; return true;
case 'lineJoin': case 'lineJoin':
if (!this.assertType(nextValue, 'string', key)) return; if (!this.assertType(nextValue, 'string', key)) return false;
this.lineJoin = nextValue; this.lineJoin = nextValue;
this.update(); this.update();
break; return true;
case 'lineCap': case 'lineCap':
if (!this.assertType(nextValue, 'string', key)) return; if (!this.assertType(nextValue, 'string', key)) return false;
this.lineCap = nextValue; this.lineCap = nextValue;
this.update(); this.update();
break; return true;
case 'miterLimit': case 'miterLimit':
if (!this.assertType(nextValue, 'number', key)) return; if (!this.assertType(nextValue, 'number', key)) return false;
this.miterLimit = nextValue; this.miterLimit = nextValue;
this.update(); 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(); this.update();
} }
patchProp( protected handleProps(
key: string, key: string,
prevValue: any, prevValue: any,
nextValue: any, nextValue: any
namespace?: ElementNamespace, ): boolean {
parentComponent?: ComponentInternalInstance | null
): void {
switch (key) { switch (key) {
case 'radius': case 'radius':
if (!this.assertType(nextValue, 'number', key)) return; if (!this.assertType(nextValue, 'number', key)) return false;
this.setRadius(nextValue); this.setRadius(nextValue);
return; return true;
case 'start': case 'start':
if (!this.assertType(nextValue, 'number', key)) return; if (!this.assertType(nextValue, 'number', key)) return false;
this.setAngle(nextValue, this.end); this.setAngle(nextValue, this.end);
return; return true;
case 'end': case 'end':
if (!this.assertType(nextValue, 'number', key)) return; if (!this.assertType(nextValue, 'number', key)) return false;
this.setAngle(this.start, nextValue); this.setAngle(this.start, nextValue);
return; return true;
case 'circle': { case 'circle': {
const value = nextValue as CircleParams; 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; const [cx, cy, radius, start, end] = value;
if (!isNil(cx) && !isNil(cy)) { if (!isNil(cx) && !isNil(cy)) {
this.pos(cx, cy); this.pos(cx, cy);
@ -440,10 +441,10 @@ export class Circle extends GraphicItemBase {
if (!isNil(start) && !isNil(end)) { if (!isNil(start) && !isNil(end)) {
this.setAngle(start, 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(); this.update();
} }
patchProp( protected handleProps(
key: string, key: string,
prevValue: any, prevValue: any,
nextValue: any, nextValue: any
namespace?: ElementNamespace, ): boolean {
parentComponent?: ComponentInternalInstance | null
): void {
switch (key) { switch (key) {
case 'radiusX': case 'radiusX':
if (!this.assertType(nextValue, 'number', key)) return; if (!this.assertType(nextValue, 'number', key)) return false;
this.setRadius(nextValue, this.radiusY); this.setRadius(nextValue, this.radiusY);
return; return true;
case 'radiusY': case 'radiusY':
if (!this.assertType(nextValue, 'number', key)) return; if (!this.assertType(nextValue, 'number', key)) return false;
this.setRadius(this.radiusY, nextValue); this.setRadius(this.radiusY, nextValue);
return; return true;
case 'start': case 'start':
if (!this.assertType(nextValue, 'number', key)) return; if (!this.assertType(nextValue, 'number', key)) return false;
this.setAngle(nextValue, this.end); this.setAngle(nextValue, this.end);
return; return true;
case 'end': case 'end':
if (!this.assertType(nextValue, 'number', key)) return; if (!this.assertType(nextValue, 'number', key)) return false;
this.setAngle(this.start, nextValue); this.setAngle(this.start, nextValue);
return; return true;
case 'ellipse': { case 'ellipse': {
const value = nextValue as EllipseParams; 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; const [cx, cy, radiusX, radiusY, start, end] = value;
if (!isNil(cx) && !isNil(cy)) { if (!isNil(cx) && !isNil(cy)) {
this.pos(cx, cy); this.pos(cx, cy);
@ -531,10 +530,10 @@ export class Ellipse extends GraphicItemBase {
if (!isNil(start) && !isNil(end)) { if (!isNil(start) && !isNil(end)) {
this.setAngle(start, 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; this.pathDirty = true;
} }
patchProp( protected handleProps(
key: string, key: string,
prevValue: any, prevValue: any,
nextValue: any, nextValue: any
namespace?: ElementNamespace, ): boolean {
parentComponent?: ComponentInternalInstance | null
): void {
switch (key) { switch (key) {
case 'x1': case 'x1':
if (!this.assertType(nextValue, 'number', key)) return; if (!this.assertType(nextValue, 'number', key)) return false;
this.setPoint1(nextValue, this.y1); this.setPoint1(nextValue, this.y1);
return; return true;
case 'y1': case 'y1':
if (!this.assertType(nextValue, 'number', key)) return; if (!this.assertType(nextValue, 'number', key)) return false;
this.setPoint1(this.x1, nextValue); this.setPoint1(this.x1, nextValue);
return; return true;
case 'x2': case 'x2':
if (!this.assertType(nextValue, 'number', key)) return; if (!this.assertType(nextValue, 'number', key)) return false;
this.setPoint2(nextValue, this.y2); this.setPoint2(nextValue, this.y2);
return; return true;
case 'y2': case 'y2':
if (!this.assertType(nextValue, 'number', key)) return; if (!this.assertType(nextValue, 'number', key)) return false;
this.setPoint2(this.x2, nextValue); this.setPoint2(this.x2, nextValue);
return; return true;
case 'line': 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.setPoint1(nextValue[0], nextValue[1]);
this.setPoint2(nextValue[2], nextValue[3]); 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(); this.update();
} }
protected isActionInElement(x: number, y: number): boolean {
return x >= 0 && x < this.width && y >= 0 && y < this.height;
}
private fitRect() { private fitRect() {
const left = Math.min(this.sx, this.cp1x, this.cp2x, this.ex); const left = Math.min(this.sx, this.cp1x, this.cp2x, this.ex);
const top = Math.min(this.sy, this.cp1y, this.cp2y, this.ey); const top = Math.min(this.sy, this.cp1y, this.cp2y, this.ey);
@ -695,55 +698,55 @@ export class BezierCurve extends GraphicItemBase {
this.pathDirty = true; this.pathDirty = true;
} }
patchProp( protected handleProps(
key: string, key: string,
prevValue: any, prevValue: any,
nextValue: any, nextValue: any
namespace?: ElementNamespace, ): boolean {
parentComponent?: ComponentInternalInstance | null
): void {
switch (key) { switch (key) {
case 'sx': case 'sx':
if (!this.assertType(nextValue, 'number', key)) return; if (!this.assertType(nextValue, 'number', key)) return false;
this.setStart(nextValue, this.sy); this.setStart(nextValue, this.sy);
return; return true;
case 'sy': case 'sy':
if (!this.assertType(nextValue, 'number', key)) return; if (!this.assertType(nextValue, 'number', key)) return false;
this.setStart(this.sx, nextValue); this.setStart(this.sx, nextValue);
return; return true;
case 'cp1x': case 'cp1x':
if (!this.assertType(nextValue, 'number', key)) return; if (!this.assertType(nextValue, 'number', key)) return false;
this.setControl1(nextValue, this.cp1y); this.setControl1(nextValue, this.cp1y);
return; return true;
case 'cp1y': case 'cp1y':
if (!this.assertType(nextValue, 'number', key)) return; if (!this.assertType(nextValue, 'number', key)) return false;
this.setControl1(this.cp1x, nextValue); this.setControl1(this.cp1x, nextValue);
return; return true;
case 'cp2x': case 'cp2x':
if (!this.assertType(nextValue, 'number', key)) return; if (!this.assertType(nextValue, 'number', key)) return false;
this.setControl2(nextValue, this.cp2y); this.setControl2(nextValue, this.cp2y);
return; return true;
case 'cp2y': case 'cp2y':
if (!this.assertType(nextValue, 'number', key)) return; if (!this.assertType(nextValue, 'number', key)) return false;
this.setControl2(this.cp2x, nextValue); this.setControl2(this.cp2x, nextValue);
return; return true;
case 'ex': case 'ex':
if (!this.assertType(nextValue, 'number', key)) return; if (!this.assertType(nextValue, 'number', key)) return false;
this.setEnd(nextValue, this.ey); this.setEnd(nextValue, this.ey);
return; return true;
case 'ey': case 'ey':
if (!this.assertType(nextValue, 'number', key)) return; if (!this.assertType(nextValue, 'number', key)) return false;
this.setEnd(this.ex, nextValue); this.setEnd(this.ex, nextValue);
return; return true;
case 'curve': 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.setStart(nextValue[0], nextValue[1]);
this.setControl1(nextValue[2], nextValue[3]); this.setControl1(nextValue[2], nextValue[3]);
this.setControl2(nextValue[4], nextValue[5]); this.setControl2(nextValue[4], nextValue[5]);
this.setEnd(nextValue[6], nextValue[7]); 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; 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, key: string,
prevValue: any, prevValue: any,
nextValue: any, nextValue: any
namespace?: ElementNamespace, ): boolean {
parentComponent?: ComponentInternalInstance | null
): void {
switch (key) { switch (key) {
case 'sx': case 'sx':
if (!this.assertType(nextValue, 'number', key)) return; if (!this.assertType(nextValue, 'number', key)) return false;
this.setStart(nextValue, this.sy); this.setStart(nextValue, this.sy);
return; return true;
case 'sy': case 'sy':
if (!this.assertType(nextValue, 'number', key)) return; if (!this.assertType(nextValue, 'number', key)) return false;
this.setStart(this.sx, nextValue); this.setStart(this.sx, nextValue);
return; return true;
case 'cpx': case 'cpx':
if (!this.assertType(nextValue, 'number', key)) return; if (!this.assertType(nextValue, 'number', key)) return false;
this.setControl(nextValue, this.cpy); this.setControl(nextValue, this.cpy);
return; return true;
case 'cpy': case 'cpy':
if (!this.assertType(nextValue, 'number', key)) return; if (!this.assertType(nextValue, 'number', key)) return false;
this.setControl(this.cpx, nextValue); this.setControl(this.cpx, nextValue);
return; return true;
case 'ex': case 'ex':
if (!this.assertType(nextValue, 'number', key)) return; if (!this.assertType(nextValue, 'number', key)) return false;
this.setEnd(nextValue, this.ey); this.setEnd(nextValue, this.ey);
return; return true;
case 'ey': case 'ey':
if (!this.assertType(nextValue, 'number', key)) return; if (!this.assertType(nextValue, 'number', key)) return false;
this.setEnd(this.ex, nextValue); this.setEnd(this.ex, nextValue);
return; return true;
case 'curve': 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.setStart(nextValue[0], nextValue[1]);
this.setControl(nextValue[2], nextValue[3]); this.setControl(nextValue[2], nextValue[3]);
this.setEnd(nextValue[4], nextValue[5]); 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(); 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, key: string,
prevValue: any, prevValue: any,
nextValue: any, nextValue: any
namespace?: ElementNamespace, ): boolean {
parentComponent?: ComponentInternalInstance | null
): void {
switch (key) { switch (key) {
case 'path': case 'path':
if (!this.assertType(nextValue, Path2D, key)) return; if (!this.assertType(nextValue, Path2D, key)) return false;
this.path = nextValue; this.path = nextValue;
this.pathDirty = true; this.pathDirty = true;
this.update(); 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, key: string,
prevValue: any, prevValue: any,
nextValue: any, nextValue: any
namespace?: ElementNamespace, ): boolean {
parentComponent?: ComponentInternalInstance | null
): void {
switch (key) { switch (key) {
case 'circle': { case 'circle': {
const value = nextValue as RectRCircleParams; const value = nextValue as RectRCircleParams;
if (!this.assertType(value, Array, key)) return; if (!this.assertType(value, Array, key)) return false;
this.setCircle(value); this.setCircle(value);
return; return true;
} }
case 'ellipse': { case 'ellipse': {
const value = nextValue as RectREllipseParams; const value = nextValue as RectREllipseParams;
if (!this.assertType(value, Array, key)) return; if (!this.assertType(value, Array, key)) return false;
this.setEllipse(value); 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 { Transform } from '../transform';
import { LayerFloorBinder, LayerGroupFloorBinder } from './floor'; import { LayerFloorBinder, LayerGroupFloorBinder } from './floor';
import { RenderAdapter } from '../adapter'; import { RenderAdapter } from '../adapter';
import { ElementNamespace, ComponentInternalInstance } from 'vue';
import { IAnimateFrame, renderEmits } from '../frame'; import { IAnimateFrame, renderEmits } from '../frame';
export interface ILayerGroupRenderExtends { export interface ILayerGroupRenderExtends {
@ -332,36 +331,34 @@ export class LayerGroup
} }
} }
patchProp( protected handleProps(
key: string, key: string,
prevValue: any, _prevValue: any,
nextValue: any, nextValue: any
namespace?: ElementNamespace, ): boolean {
parentComponent?: ComponentInternalInstance | null
): void {
switch (key) { switch (key) {
case 'cellSize': case 'cellSize':
if (!this.assertType(nextValue, 'number', key)) return; if (!this.assertType(nextValue, 'number', key)) return false;
this.setCellSize(nextValue); this.setCellSize(nextValue);
return; return true;
case 'blockSize': case 'blockSize':
if (!this.assertType(nextValue, 'number', key)) return; if (!this.assertType(nextValue, 'number', key)) return false;
this.setBlockSize(nextValue); this.setBlockSize(nextValue);
return; return true;
case 'floorId': { case 'floorId': {
if (!this.assertType(nextValue, 'number', key)) return; if (!this.assertType(nextValue, 'number', key)) return false;
const binder = this.getExtends('floor-binder'); const binder = this.getExtends('floor-binder');
if (binder instanceof LayerGroupFloorBinder) { if (binder instanceof LayerGroupFloorBinder) {
binder.bindFloor(nextValue); binder.bindFloor(nextValue);
} }
return; return true;
} }
case 'camera': case 'camera':
if (!this.assertType(nextValue, Transform, key)) return; if (!this.assertType(nextValue, Transform, key)) return false;
this.camera = nextValue; this.camera = nextValue;
return; return true;
} }
super.patchProp(key, prevValue, nextValue, namespace, parentComponent); return false;
} }
destroy(): void { destroy(): void {
@ -1438,16 +1435,14 @@ export class Layer extends Container<ELayerEvent> {
}); });
} }
patchProp( protected handleProps(
key: string, key: string,
prevValue: any, _prevValue: any,
nextValue: any, nextValue: any
namespace?: ElementNamespace, ): boolean {
parentComponent?: ComponentInternalInstance | null
): void {
switch (key) { switch (key) {
case 'layer': { case 'layer': {
if (!this.assertType(nextValue, 'string', key)) return; if (!this.assertType(nextValue, 'string', key)) return false;
const parent = this.parent; const parent = this.parent;
if (parent instanceof LayerGroup) { if (parent instanceof LayerGroup) {
parent.removeLayer(this); parent.removeLayer(this);
@ -1457,30 +1452,30 @@ export class Layer extends Container<ELayerEvent> {
this.layer = nextValue; this.layer = nextValue;
} }
this.update(); this.update();
return; return true;
} }
case 'cellSize': case 'cellSize':
if (!this.assertType(nextValue, 'number', key)) return; if (!this.assertType(nextValue, 'number', key)) return false;
this.setCellSize(nextValue); this.setCellSize(nextValue);
return; return true;
case 'mapWidth': case 'mapWidth':
if (!this.assertType(nextValue, 'number', key)) return; if (!this.assertType(nextValue, 'number', key)) return false;
this.setMapSize(nextValue, this.mapHeight); this.setMapSize(nextValue, this.mapHeight);
return; return true;
case 'mapHeight': case 'mapHeight':
if (!this.assertType(nextValue, 'number', key)) return; if (!this.assertType(nextValue, 'number', key)) return false;
this.setMapSize(this.mapWidth, nextValue); this.setMapSize(this.mapWidth, nextValue);
return; return true;
case 'background': case 'background':
if (!this.assertType(nextValue, 'number', key)) return; if (!this.assertType(nextValue, 'number', key)) return false;
this.setBackground(nextValue); this.setBackground(nextValue);
return; return true;
case 'floorImage': case 'floorImage':
if (!this.assertType(nextValue, Array, key)) return; if (!this.assertType(nextValue, Array, key)) return false;
this.setFloorImage(nextValue as FloorAnimate[]); this.setFloorImage(nextValue as FloorAnimate[]);
return; return true;
} }
super.patchProp(key, prevValue, nextValue, namespace, parentComponent); return false;
} }
private addToGroup(group: LayerGroup) { private addToGroup(group: LayerGroup) {

View File

@ -1,7 +1,6 @@
import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d'; import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
import { ERenderItemEvent, RenderItem, RenderItemPosition } from '../item'; import { ERenderItemEvent, RenderItem, RenderItemPosition } from '../item';
import { Transform } from '../transform'; import { Transform } from '../transform';
import { ElementNamespace, ComponentInternalInstance } from 'vue';
import { AutotileRenderable, RenderableData } from '../cache'; import { AutotileRenderable, RenderableData } from '../cache';
import { texture } from '../cache'; import { texture } from '../cache';
import { isNil } from 'lodash-es'; import { isNil } from 'lodash-es';
@ -114,33 +113,31 @@ export class Text extends RenderItem<ETextEvent> {
this.size(width, actualBoundingBoxAscent + actualBoundingBoxDescent); this.size(width, actualBoundingBoxAscent + actualBoundingBoxDescent);
} }
patchProp( protected handleProps(
key: string, key: string,
prevValue: any, _prevValue: any,
nextValue: any, nextValue: any
namespace?: ElementNamespace, ): boolean {
parentComponent?: ComponentInternalInstance | null
): void {
switch (key) { switch (key) {
case 'text': case 'text':
if (!this.assertType(nextValue, 'string', key)) return; if (!this.assertType(nextValue, 'string', key)) return false;
this.setText(nextValue); this.setText(nextValue);
return; return true;
case 'fillStyle': case 'fillStyle':
this.setStyle(nextValue, this.strokeStyle); this.setStyle(nextValue, this.strokeStyle);
return; return true;
case 'strokeStyle': case 'strokeStyle':
this.setStyle(this.fillStyle, nextValue); this.setStyle(this.fillStyle, nextValue);
return; return true;
case 'font': case 'font':
if (!this.assertType(nextValue, 'string', key)) return; if (!this.assertType(nextValue, 'string', key)) return false;
this.setFont(nextValue); this.setFont(nextValue);
break; break;
case 'strokeWidth': case 'strokeWidth':
this.setStrokeWidth(nextValue); 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(); this.update();
} }
patchProp( protected handleProps(
key: string, key: string,
prevValue: any, _prevValue: any,
nextValue: any, nextValue: any
namespace?: ElementNamespace, ): boolean {
parentComponent?: ComponentInternalInstance | null
): void {
switch (key) { switch (key) {
case 'image': case 'image':
this.setImage(nextValue); 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, _canvas: MotaOffscreenCanvas2D,
_transform: Transform _transform: Transform
): void {} ): void {}
protected handleProps(): boolean {
return false;
}
} }
export interface EIconEvent extends ERenderItemEvent {} export interface EIconEvent extends ERenderItemEvent {}
@ -313,31 +312,29 @@ export class Icon extends RenderItem<EIconEvent> implements IAnimateFrame {
super.destroy(); super.destroy();
} }
patchProp( protected handleProps(
key: string, key: string,
prevValue: any, _prevValue: any,
nextValue: any, nextValue: any
namespace?: ElementNamespace, ): boolean {
parentComponent?: ComponentInternalInstance | null
): void {
switch (key) { switch (key) {
case 'icon': case 'icon':
this.setIcon(nextValue); this.setIcon(nextValue);
return; return true;
case 'animate': case 'animate':
if (!this.assertType(nextValue, 'boolean', key)) return; if (!this.assertType(nextValue, 'boolean', key)) return false;
this.animate = nextValue; this.animate = nextValue;
if (nextValue) renderEmits.addFramer(this); if (nextValue) renderEmits.addFramer(this);
else renderEmits.removeFramer(this); else renderEmits.removeFramer(this);
this.update(); this.update();
return; return true;
case 'frame': case 'frame':
if (!this.assertType(nextValue, 'number', key)) return; if (!this.assertType(nextValue, 'number', key)) return false;
this.frame = nextValue; this.frame = nextValue;
this.update(); 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(); this.update();
} }
patchProp( protected handleProps(
key: string, key: string,
prevValue: any, _prevValue: any,
nextValue: any, nextValue: any
namespace?: ElementNamespace, ): boolean {
parentComponent?: ComponentInternalInstance | null
): void {
switch (key) { switch (key) {
case 'image': case 'image':
if (!this.assertType(nextValue, 'string', key)) return; if (!this.assertType(nextValue, 'string', key)) return false;
this.setImageByName(nextValue); this.setImageByName(nextValue);
return; return true;
case 'borderSize': case 'borderSize':
if (!this.assertType(nextValue, 'number', key)) return; if (!this.assertType(nextValue, 'number', key)) return false;
this.setBorderSize(nextValue); 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 { MotaOffscreenCanvas2D } from '../fx/canvas2d';
import { Container } from './container'; import { Container } from './container';
import { import {
ActionEventMap,
ActionType, ActionType,
IActionEvent, IActionEvent,
IActionEventBase,
IWheelEvent, IWheelEvent,
MouseType, MouseType,
WheelType WheelType
@ -49,6 +49,10 @@ export class MotaRenderer extends Container implements IRenderTreeRoot {
private abort?: AbortController; private abort?: AbortController;
/** 根据捕获行为判断光标样式 */ /** 根据捕获行为判断光标样式 */
private targetCursor: string = 'auto'; private targetCursor: string = 'auto';
/** 当前鼠标覆盖的元素 */
private hoveredElement: Set<RenderItem> = new Set();
/** 本次交互前鼠标覆盖的元素 */
private beforeHovered: Set<RenderItem> = new Set();
target!: MotaOffscreenCanvas2D; target!: MotaOffscreenCanvas2D;
@ -112,17 +116,26 @@ export class MotaRenderer extends Container implements IRenderTreeRoot {
this.lastMouse this.lastMouse
); );
this.targetCursor = 'auto'; this.targetCursor = 'auto';
const temp = this.beforeHovered;
temp.clear();
this.beforeHovered = this.hoveredElement;
this.hoveredElement = temp;
this.captureEvent(ActionType.Move, event); this.captureEvent(ActionType.Move, event);
}); if (this.targetCursor !== this.target.canvas.style.cursor) {
canvas.addEventListener('mouseenter', ev => { this.target.canvas.style.cursor = this.targetCursor;
const event = this.createMouseAction(ev, ActionType.Enter); }
this.emit('enterCapture', event); this.checkMouseEnterLeave(
this.emit('enter', event); ev,
this.beforeHovered,
this.hoveredElement
);
}); });
canvas.addEventListener('mouseleave', ev => { canvas.addEventListener('mouseleave', ev => {
const event = this.createMouseAction(ev, ActionType.Leave); this.hoveredElement.forEach(v => {
this.emit('leaveCapture', event); v.emit('leave', this.createMouseActionBase(ev, v));
this.emit('leave', event); });
this.hoveredElement.clear();
this.beforeHovered.clear();
}); });
document.addEventListener('touchstart', ev => { document.addEventListener('touchstart', ev => {
this.createTouchAction(ev, ActionType.Down).forEach(v => { 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.createTouchAction(ev, ActionType.Up).forEach(v => {
this.captureEvent(ActionType.Up, v); this.captureEvent(ActionType.Up, v);
this.captureEvent(ActionType.Click, v); this.captureEvent(ActionType.Click, v);
this.touchInfo.delete(v.identifier);
}); });
}); });
document.addEventListener('touchcancel', ev => { document.addEventListener('touchcancel', ev => {
this.createTouchAction(ev, ActionType.Up).forEach(v => { this.createTouchAction(ev, ActionType.Up).forEach(v => {
this.captureEvent(ActionType.Up, v); this.captureEvent(ActionType.Up, v);
this.touchInfo.delete(v.identifier);
}); });
}); });
document.addEventListener('touchmove', ev => { document.addEventListener('touchmove', ev => {
this.createTouchAction(ev, ActionType.Move).forEach(v => { this.createTouchAction(ev, ActionType.Move).forEach(v => {
const touch = this.touchInfo.get(v.identifier); const touch = this.touchInfo.get(v.identifier);
if (!touch) return; if (!touch) return;
const inElement = this.isTouchInCanvas(v.offsetX, v.offsetY); const temp = this.beforeHovered;
if (touch.hovered && !inElement) { temp.clear();
this.emit('leaveCapture', v); this.beforeHovered = this.hoveredElement;
this.emit('leave', v); this.hoveredElement = temp;
}
if (!touch.hovered && inElement) {
this.emit('enterCapture', v);
this.emit('enter', v);
}
this.captureEvent(ActionType.Move, v); this.captureEvent(ActionType.Move, v);
this.checkTouchEnterLeave(
ev,
this.beforeHovered,
this.hoveredElement
);
}); });
}); });
canvas.addEventListener('wheel', ev => { canvas.addEventListener('wheel', ev => {
@ -246,6 +261,39 @@ export class MotaRenderer extends Container implements IRenderTreeRoot {
return buttons; 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( private createMouseAction(
event: MouseEvent, event: MouseEvent,
type: ActionType, type: ActionType,
@ -385,14 +433,40 @@ export class MotaRenderer extends Container implements IRenderTreeRoot {
return list; return list;
} }
bubbleEvent<T extends ActionType>( private checkMouseEnterLeave(
type: T, event: MouseEvent,
event: ActionEventMap[T] before: Set<RenderItem>,
): ActionEventMap[T] | null { now: Set<RenderItem>
if (this.targetCursor !== this.target.canvas.style.cursor) { ) {
this.target.canvas.style.cursor = this.targetCursor; // 先 leave再 enter
} before.forEach(v => {
return super.bubbleEvent(type, event); 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) { update(_item: RenderItem = this) {
@ -465,9 +539,10 @@ export class MotaRenderer extends Container implements IRenderTreeRoot {
} }
hoverElement(element: RenderItem): void { hoverElement(element: RenderItem): void {
if (element.cursor !== 'auto') { if (element.cursor !== 'inherit') {
this.targetCursor = element.cursor; this.targetCursor = element.cursor;
} }
this.hoveredElement.add(element);
} }
destroy() { destroy() {

View File

@ -76,7 +76,7 @@ type _Define<P extends BaseProps, E extends ERenderItemEvent> = DefineComponent<
Readonly<P & MappingEvent<E>> 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> & MappingEvent<E> &
ReservedProps; ReservedProps;

View File

@ -8,6 +8,13 @@ import { ERenderItemEvent, RenderItem } from '../item';
import { tagMap } from './map'; import { tagMap } from './map';
import { logger } from '@/core/common/logger'; import { logger } from '@/core/common/logger';
import { Comment, ETextEvent, Text } from '../preset/misc'; 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>({ export const { createApp, render } = createRenderer<RenderItem, RenderItem>({
patchProp: function ( patchProp: function (

View File

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

View File

@ -6,7 +6,6 @@ import {
} from './item'; } from './item';
import { MotaOffscreenCanvas2D } from '../fx/canvas2d'; import { MotaOffscreenCanvas2D } from '../fx/canvas2d';
import { Transform } from './transform'; import { Transform } from './transform';
import { ElementNamespace, ComponentInternalInstance } from 'vue';
export interface ESpriteEvent extends ERenderItemEvent {} export interface ESpriteEvent extends ERenderItemEvent {}
@ -42,19 +41,17 @@ export class Sprite<
this.update(this); this.update(this);
} }
patchProp( protected handleProps(
key: string, key: string,
prevValue: any, _prevValue: any,
nextValue: any, nextValue: any
namespace?: ElementNamespace, ): boolean {
parentComponent?: ComponentInternalInstance | null
): void {
switch (key) { switch (key) {
case 'render': case 'render':
if (!this.assertType(nextValue, 'function', key)) return; if (!this.assertType(nextValue, 'function', key)) return false;
this.setRenderFn(nextValue); 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 { defineComponent, ref, watch } from 'vue';
import { SetupComponentOptions } from './types'; import { SetupComponentOptions } from './types';
import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d'; import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
interface ProgressProps { interface ProgressProps extends DefaultProps {
/** 进度条的位置 */ /** 进度条的位置 */
loc: ElementLocator; loc: ElementLocator;
/** 进度条的进度1表示完成0表示未完成 */ /** 进度条的进度1表示完成0表示未完成 */

View File

@ -10,12 +10,12 @@ import {
} from 'vue'; } from 'vue';
import { SetupComponentOptions } from './types'; import { SetupComponentOptions } from './types';
import { clamp } from 'lodash-es'; import { clamp } from 'lodash-es';
import { ElementLocator } from '@/core/render'; import { DefaultProps, ElementLocator } from '@/core/render';
/** 圆角矩形页码距离容器的边框大小,与 pageSize 相乘 */ /** 圆角矩形页码距离容器的边框大小,与 pageSize 相乘 */
const RECT_PAD = 0.1; const RECT_PAD = 0.1;
export interface PageProps { export interface PageProps extends DefaultProps {
/** 共有多少页 */ /** 共有多少页 */
pages: number; pages: number;
/** 页码组件的定位 */ /** 页码组件的定位 */
@ -40,6 +40,23 @@ const pageProps = {
props: ['pages', 'loc', 'pageSize'] props: ['pages', 'loc', 'pageSize']
} satisfies SetupComponentOptions<PageProps, {}, string, PageSlots>; } 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>( export const Page = defineComponent<PageProps, {}, string, PageSlots>(
(props, { slots, expose }) => { (props, { slots, expose }) => {
const nowPage = ref(1); const nowPage = ref(1);
@ -60,8 +77,8 @@ export const Page = defineComponent<PageProps, {}, string, PageSlots>(
const textLoc = ref<ElementLocator>([0, 0, 0, 0]); const textLoc = ref<ElementLocator>([0, 0, 0, 0]);
// 两个监听的参数 // 两个监听的参数
const leftArrow = ref<Path2D>(new Path2D()); const leftArrow = ref<Path2D>();
const rightArrow = ref<Path2D>(new Path2D()); const rightArrow = ref<Path2D>();
const isFirst = computed(() => nowPage.value === 1); const isFirst = computed(() => nowPage.value === 1);
const isLast = computed(() => nowPage.value === props.pages); const isLast = computed(() => nowPage.value === props.pages);

View File

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

View File

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

View File

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