|
|
@@ -42,8 +42,8 @@ export namespace MiniRender {
|
|
|
anchorY: number; // 0..1 (relative to height)
|
|
|
}
|
|
|
|
|
|
- export type RenderObjectEventName = "added" | "removed";
|
|
|
- export type RenderObjectEventHandler = (obj: RenderObject) => void;
|
|
|
+ export type RenderObjectEventName = "added" | "removed" | "click" | "touchstart" | "touchmove" | "touchend" | "touchcancel";
|
|
|
+ export type RenderObjectEventHandler = (obj: RenderObject, data?: any) => void;
|
|
|
|
|
|
export class RenderObject implements TransformLike, IRenderable, IUpdatable {
|
|
|
public readonly id: RenderObjectId;
|
|
|
@@ -62,6 +62,7 @@ export namespace MiniRender {
|
|
|
|
|
|
public visible = true;
|
|
|
public zIndex = 0;
|
|
|
+ public interactive = false;
|
|
|
|
|
|
public parent: Container | null = null;
|
|
|
|
|
|
@@ -86,10 +87,15 @@ export namespace MiniRender {
|
|
|
this.listeners.get(event)?.delete(handler);
|
|
|
}
|
|
|
|
|
|
- public emit(event: RenderObjectEventName): void {
|
|
|
+ public emit(event: RenderObjectEventName, data?: any): void {
|
|
|
const set = this.listeners.get(event);
|
|
|
if (!set) return;
|
|
|
- for (const h of set) h(this);
|
|
|
+ for (const h of set) h(this, data);
|
|
|
+ }
|
|
|
+
|
|
|
+ public hasListener(event: RenderObjectEventName): boolean {
|
|
|
+ const set = this.listeners.get(event);
|
|
|
+ return !!set && set.size > 0;
|
|
|
}
|
|
|
|
|
|
protected withTransform(rc: RenderContext, draw: () => void): void {
|
|
|
@@ -119,6 +125,45 @@ export namespace MiniRender {
|
|
|
}
|
|
|
|
|
|
public update(_dtMs: number): void {}
|
|
|
+
|
|
|
+ public parentToLocal(px: number, py: number): { x: number; y: number } | null {
|
|
|
+ const ax = this.anchorX * this.width;
|
|
|
+ const ay = this.anchorY * this.height;
|
|
|
+
|
|
|
+ let lx = px - (this.x + ax);
|
|
|
+ let ly = py - (this.y + ay);
|
|
|
+
|
|
|
+ if (this.rotation) {
|
|
|
+ const cos = Math.cos(-this.rotation);
|
|
|
+ const sin = Math.sin(-this.rotation);
|
|
|
+ const rx = lx * cos - ly * sin;
|
|
|
+ const ry = lx * sin + ly * cos;
|
|
|
+ lx = rx;
|
|
|
+ ly = ry;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (this.scaleX === 0 || this.scaleY === 0) return null;
|
|
|
+ lx /= this.scaleX;
|
|
|
+ ly /= this.scaleY;
|
|
|
+
|
|
|
+ lx += ax;
|
|
|
+ ly += ay;
|
|
|
+
|
|
|
+ return { x: lx, y: ly };
|
|
|
+ }
|
|
|
+
|
|
|
+ public hitTest(px: number, py: number): RenderObject | null {
|
|
|
+ if (!this.visible || this.alpha <= 0) return null;
|
|
|
+ if (!this.interactive) return null;
|
|
|
+
|
|
|
+ const local = this.parentToLocal(px, py);
|
|
|
+ if (!local) return null;
|
|
|
+
|
|
|
+ if (local.x >= 0 && local.x <= this.width && local.y >= 0 && local.y <= this.height) {
|
|
|
+ return this;
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
export class Container extends RenderObject {
|
|
|
@@ -168,6 +213,26 @@ export namespace MiniRender {
|
|
|
protected override draw(rc: RenderContext): void {
|
|
|
for (const c of this._children) c.render(rc);
|
|
|
}
|
|
|
+
|
|
|
+ public override hitTest(px: number, py: number): RenderObject | null {
|
|
|
+ if (!this.visible || this.alpha <= 0) return null;
|
|
|
+
|
|
|
+ const local = this.parentToLocal(px, py);
|
|
|
+ if (!local) return null;
|
|
|
+
|
|
|
+ for (let i = this._children.length - 1; i >= 0; i--) {
|
|
|
+ const hit = this._children[i].hitTest(local.x, local.y);
|
|
|
+ if (hit) return hit;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (this.interactive && this.hasListener("click")) {
|
|
|
+ if (local.x >= 0 && local.x <= this.width && local.y >= 0 && local.y <= this.height) {
|
|
|
+ return this;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return null;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
export class Rect extends RenderObject {
|
|
|
@@ -601,11 +666,42 @@ export namespace MiniRender {
|
|
|
|
|
|
private rafId: number | null = null;
|
|
|
private lastTs: number | null = null;
|
|
|
+ private currentDragObject: RenderObject | null = null;
|
|
|
+
|
|
|
public readonly canvas: RendeCanvasInterface;
|
|
|
public readonly width: number;
|
|
|
public readonly height: number;
|
|
|
public readonly backgroundColor: string;
|
|
|
|
|
|
+
|
|
|
+ public readonly events = {
|
|
|
+ handleTouchStart: (e: any) => {
|
|
|
+ const pos = this.getEventPosition(e);
|
|
|
+ const hit = this.root.hitTest(pos.x, pos.y);
|
|
|
+ if (hit) {
|
|
|
+ hit.emit("touchstart", e);
|
|
|
+ this.currentDragObject = hit;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ handleTouchMove: (e: any) => {
|
|
|
+ if (this.currentDragObject) {
|
|
|
+ this.currentDragObject.emit("touchmove", e);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ handleTouchEnd: (e: any) => {
|
|
|
+ if (this.currentDragObject) {
|
|
|
+ this.currentDragObject.emit("touchend", e);
|
|
|
+ this.currentDragObject.emit("click", e);
|
|
|
+ this.currentDragObject = null;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ handleTouchCancel: (e: any) => {
|
|
|
+ if (this.currentDragObject)
|
|
|
+ this.currentDragObject.emit("touchcancel", e);
|
|
|
+ this.currentDragObject = null;
|
|
|
+ },
|
|
|
+ };
|
|
|
+
|
|
|
constructor(
|
|
|
public options: {
|
|
|
canvas: RendeCanvasInterface,
|
|
|
@@ -675,5 +771,12 @@ export namespace MiniRender {
|
|
|
this.assets.clear();
|
|
|
this.root.removeAll();
|
|
|
}
|
|
|
+
|
|
|
+ private getEventPosition(e: any): { x: number; y: number } {
|
|
|
+ if (e?.detail?.x !== undefined) return { x: e.detail.x, y: e.detail.y };
|
|
|
+ if (e?.offsetX !== undefined) return { x: e.offsetX, y: e.offsetY };
|
|
|
+ if (e?.touches?.[0]) return { x: e.touches[0].x, y: e.touches[0].y };
|
|
|
+ return { x: 0, y: 0 };
|
|
|
+ }
|
|
|
}
|
|
|
}
|