Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions src/elements/mixable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -28,6 +51,27 @@ export class CustomHTMLElement extends HTMLElement {
return element;
}

#eventListeners = new Map<keyof HTMLElementEventMap, EventListener>();

#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
*/
Expand Down
24 changes: 1 addition & 23 deletions src/elements/visual/c2dBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<EventName>
) => 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<E extends keyof HTMLElementEventMap>(_: never, eventName: E) {
return (listener: TypedEventListener<E>) =>
element.addEventListener(eventName, listener);
},
});
})();
#everyFrame: Updater | null = null;

/**
Expand Down Expand Up @@ -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.
Expand Down
74 changes: 57 additions & 17 deletions src/elements/visual/renderable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<E extends keyof HTMLElementEventMap> = {
eventName: E;
listener: TypedEventListener<E>;
};

export class Canvas2DBaseRenderable extends C2DBase {
#changedSinceRender = false;
#clickListeners = new Set<EventListenerOrEventListenerObject>();
#clickListeners = new Set<TypedEventListener<"click">>();
#localMouse = new MouseData();
#mouseListeners = new Set<EventListenerOrEventListenerObject>();
#mouseListeners = new Set<TypedEventListener<"mousemove">>();
#shadow: Shadow | null = null;
#connected = false;
#queuedEventListeners: QueuedEventListener<any>[] = [];

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<E extends keyof HTMLElementEventMap>(
type: E,
listener: TypedEventListener<E>,
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;
}

Expand Down Expand Up @@ -105,22 +131,24 @@ export class Canvas2DBaseRenderable extends C2DBase {
/**
* @private
*/
removeEventListener(
type: keyof HTMLElementEventMap,
listener: EventListenerOrEventListenerObject,
removeEventListener<E extends keyof HTMLElementEventMap>(
type: E,
listener: TypedEventListener<E>,
options?: boolean | AddEventListenerOptions
): void {
switch (type) {
case "click":
this.#clickListeners.delete(listener);
this.#clickListeners.delete(listener as TypedEventListener<"click">);
break;
case "mousedown":
case "mouseup":
case "mouseenter":
case "mouseout":
case "mouseover":
case "mousemove":
this.#mouseListeners.delete(listener);
this.#mouseListeners.delete(
listener as TypedEventListener<"mousemove">
);
break;
}

Expand Down Expand Up @@ -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"));
Expand Down
4 changes: 4 additions & 0 deletions src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,7 @@ type Writeable<T> = {
};

type Options<T> = Partial<Writeable<T>>;

type EventListenerMap = {
[EventName in keyof HTMLElementEventMap]?: TypedEventListener<EventName>
};