diff --git a/src/elements/mixable.ts b/src/elements/mixable.ts index 296fff4..faaa20a 100644 --- a/src/elements/mixable.ts +++ b/src/elements/mixable.ts @@ -6,6 +6,29 @@ export class CustomHTMLElement extends HTMLElement { constructor(...args: any[]) { super(); + + const element = this; + + this.#eventsProxy = new Proxy({} as EventListenerMap, { + get(_, eventName: keyof HTMLElementEventMap) { + return element.#eventListeners.get(eventName); + }, + set(_, eventName: keyof HTMLElementEventMap, listener) { + const currentListener = element.#eventListeners.get(eventName); + + if (currentListener === listener) return true; + + if (currentListener !== undefined) { + element.removeEventListener(eventName, currentListener); + } + + element.addEventListener(eventName, listener); + + element.#eventListeners.set(eventName, listener); + + return true; + }, + }); } attributeChangedCallback( @@ -28,6 +51,27 @@ export class CustomHTMLElement extends HTMLElement { return element; } + #eventListeners = new Map(); + + #eventsProxy: EventListenerMap; + + get events() { + return this.#eventsProxy as EventListenerMap; + } + + set events(map) { + Object.assign(this.#eventsProxy, map); + } + + /** + * Interface for adding event listeners with alternative syntax. For example, + * element.addEventListener("click", listener) becomes + * element.listen.click(listener). + */ + get listen(): EventListenerMap { + return this.#eventsProxy; + } + /** * @private */ diff --git a/src/elements/visual/c2dBase.ts b/src/elements/visual/c2dBase.ts index ce0c47b..95cab00 100644 --- a/src/elements/visual/c2dBase.ts +++ b/src/elements/visual/c2dBase.ts @@ -2,27 +2,12 @@ import { Vector2D } from "../../classes/vector2d"; import { CustomHTMLElement } from "../mixable"; import { Canvas2DCanvasElement } from "./canvas"; -type EventListenerAdder = { - readonly [EventName in keyof HTMLElementEventMap]: ( - listener: TypedEventListener - ) => void; -}; - export class C2DBase extends CustomHTMLElement { /** * The element's custom HTML tag. This can be passed into document.createElement(). */ static tag: string; - #eventProxy = (() => { - const element = this; - return new Proxy({} as EventListenerAdder, { - get(_: never, eventName: E) { - return (listener: TypedEventListener) => - element.addEventListener(eventName, listener); - }, - }); - })(); #everyFrame: Updater | null = null; /** @@ -52,14 +37,7 @@ export class C2DBase extends CustomHTMLElement { this.#everyFrame = updater; } - /** - * Interface for adding event listeners with alternative syntax. For example, - * element.addEventListener("click", listener) becomes - * element.listen.click(listener). - */ - get listen(): EventListenerAdder { - return this.#eventProxy; - } + /** * Scales a vector by the device's pixel ratio. diff --git a/src/elements/visual/renderable.ts b/src/elements/visual/renderable.ts index a82ba12..66f911d 100644 --- a/src/elements/visual/renderable.ts +++ b/src/elements/visual/renderable.ts @@ -5,52 +5,78 @@ import { } from "../../classes/gradient"; import { MouseData } from "../../classes/mouse"; import { Shadow } from "../../classes/shadow"; -import { Vector2D } from "../../classes/vector2d"; import { c2dShapeChildren, c2dStandaloneChildren } from "../../mixins/children"; import { Canvas2DCanvasElement } from "./canvas"; import { C2DBase } from "./c2dBase"; -import { Canvas2DShape } from "./shape"; import { CustomHTMLElement } from "../mixable"; export const changedEvent = new Event("change", { bubbles: true }); +type QueuedEventListener = { + eventName: E; + listener: TypedEventListener; +}; + export class Canvas2DBaseRenderable extends C2DBase { #changedSinceRender = false; - #clickListeners = new Set(); + #clickListeners = new Set>(); #localMouse = new MouseData(); - #mouseListeners = new Set(); + #mouseListeners = new Set>(); #shadow: Shadow | null = null; + #connected = false; + #queuedEventListeners: QueuedEventListener[] = []; constructor(...args: any[]) { super(); } + connectedCallback() { + this.#connected = true; + + while (this.#queuedEventListeners.length) { + const firstListener = this.#queuedEventListeners.shift(); + + if (firstListener === undefined) break; + + this.addEventListener(firstListener.eventName, firstListener.listener); + } + } + /** * @private */ - addEventListener( - type: keyof HTMLElementEventMap, - listener: EventListenerOrEventListenerObject, + addEventListener( + type: E, + listener: TypedEventListener, options?: boolean | AddEventListenerOptions ): void { + if (!this.#connected) { + this.#queuedEventListeners.push({ + eventName: type, + listener, + }); + + return; + } + switch (type) { case "click": this.canvas.renderOn(type); - this.#clickListeners.add(listener); + this.#clickListeners.add(listener as TypedEventListener<"click">); break; case "mousedown": case "mouseup": case "mousemove": this.canvas.renderOn(type); - this.#mouseListeners.add(listener); + this.#mouseListeners.add(listener as TypedEventListener<"mousemove">); break; case "mouseenter": case "mouseout": case "mouseover": this.canvas.renderOn("mousemove"); - this.#mouseListeners.add(listener); + this.#mouseListeners.add(listener as TypedEventListener<"mousemove">); break; } @@ -105,14 +131,14 @@ export class Canvas2DBaseRenderable extends C2DBase { /** * @private */ - removeEventListener( - type: keyof HTMLElementEventMap, - listener: EventListenerOrEventListenerObject, + removeEventListener( + type: E, + listener: TypedEventListener, options?: boolean | AddEventListenerOptions ): void { switch (type) { case "click": - this.#clickListeners.delete(listener); + this.#clickListeners.delete(listener as TypedEventListener<"click">); break; case "mousedown": case "mouseup": @@ -120,7 +146,9 @@ export class Canvas2DBaseRenderable extends C2DBase { case "mouseout": case "mouseover": case "mousemove": - this.#mouseListeners.delete(listener); + this.#mouseListeners.delete( + listener as TypedEventListener<"mousemove"> + ); break; } @@ -170,8 +198,20 @@ export class Canvas2DBaseRenderable extends C2DBase { this.dispatchEvent(new MouseEvent("mouseover")); - if (!this.#localMouse.equals(mouse)) - this.dispatchEvent(new MouseEvent("mousemove")); + const movementX = mouse.x - mouse.previous.x; + + const movementY = mouse.y - mouse.previous.y; + + if ( + this.#localMouse.x !== mouse.previous.x * devicePixelRatio || + this.#localMouse.y !== mouse.previous.y * devicePixelRatio + ) + this.dispatchEvent( + new MouseEvent("mousemove", { + movementX, + movementY, + }) + ); if (!this.#localMouse.over) { this.dispatchEvent(new MouseEvent("mouseenter")); diff --git a/src/types.d.ts b/src/types.d.ts index 7a2a538..863e5b0 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -25,3 +25,7 @@ type Writeable = { }; type Options = Partial>; + +type EventListenerMap = { + [EventName in keyof HTMLElementEventMap]?: TypedEventListener +};