diff --git a/lib/Propagator.ts b/lib/Propagator.ts deleted file mode 100644 index b2f56aa..0000000 --- a/lib/Propagator.ts +++ /dev/null @@ -1,243 +0,0 @@ -/** - * A function that processes the event and updates the target. - * @param {EventTarget} source - The source that emitted the event - * @param {EventTarget} target - The target that receives propagated changes - * @param {Event} event - The event that triggered the propagation - * @returns {any} - The result of the propagation - */ -export type PropagatorFunction = (source: EventTarget, target: EventTarget, event: Event) => any; - -/** - * A parser function that converts a string expression into a PropagatorFunction. - * @param {string} body - The string expression to parse - * @param {Propagator} [propagator] - The Propagator instance (optional) - * @returns {PropagatorFunction | null} - The parsed PropagatorFunction or null if parsing fails - */ -export type PropagatorParser = (body: string, propagator?: Propagator) => PropagatorFunction | null; - -export type PropagatorOptions = { - source?: EventTarget | null; - target?: EventTarget | null; - event?: string | null; - handler?: PropagatorFunction | string; - parser?: PropagatorParser; - onParseSuccess?: (body: string) => void; - onParseError?: (error: Error) => void; -}; - -/** - * A propagator takes in a source and target and listens for events on the source. - * When an event is detected, it will execute a handler and update the target. - */ -export class Propagator { - #source: EventTarget | null = null; - #target: EventTarget | null = null; - #eventName: string | null = null; - #handler: PropagatorFunction | null = null; - - #parser: PropagatorParser | null = null; - #onParse: ((body: string) => void) | null = null; - #onError: ((error: Error) => void) | null = null; - - /** - * Creates a new Propagator instance. - * @param {PropagatorOptions} options - Configuration options for the propagator - * @param {EventTarget} [options.source] - Source that emits events - * @param {EventTarget} [options.target] - Target that receives propagated changes - * @param {string} [options.event] - Event name to listen for on the source - * @param {PropagatorFunction|string} [options.handler] - Event handler function or string expression - * @param {PropagatorParser} [options.parser] - Custom parser for string handlers - * @param {Function} [options.onParse] - Callback fired when a string handler is parsed - * @param {Function} [options.onError] - Callback fired when an error occurs during parsing - */ - constructor(options: PropagatorOptions = {}) { - const { - source = null, - target = null, - event = null, - handler = null, - onParseSuccess: onParse = null, - onParseError: onError = null, - parser = null, - } = options; - - this.#onParse = onParse; - this.#onError = onError; - this.#parser = parser; - this.source = source; - this.target = target; - if (event) this.event = event; - if (handler) this.handler = handler; - } - - /** - * The source that emits events. - * Setting a new source will automatically update event listeners. - */ - get source(): EventTarget | null { - return this.#source; - } - - set source(eventTarget: EventTarget | null) { - // Remove listener from old source - if (this.#source && this.#eventName) { - this.#source.removeEventListener(this.#eventName, this.#handleEvent); - } - - this.#source = eventTarget; - - // Add listener to new source - if (this.#source && this.#eventName) { - this.#source.addEventListener(this.#eventName, this.#handleEvent); - } - } - - /** - * The target that receives propagated changes. - */ - get target(): EventTarget | null { - return this.#target; - } - - set target(eventTarget: EventTarget | null) { - this.#target = eventTarget; - } - - /** - * The name of the event to listen for on the source. - * Setting a new event name will automatically update event listeners. - */ - get event(): string | null { - return this.#eventName; - } - - set event(name: string) { - // Remove old listener - if (this.#source && this.#eventName) { - this.#source.removeEventListener(this.#eventName, this.#handleEvent); - } - - this.#eventName = name; - - // Add new listener - if (this.#source && this.#eventName) { - this.#source.addEventListener(this.#eventName, this.#handleEvent); - } - } - - /** - * The handler function that processes the event and updates the target. - * Can be set using either a function or a string expression. - */ - get handler(): PropagatorFunction | null { - return this.#handler; - } - - set handler(value: PropagatorFunction | string | null) { - if (typeof value === 'string') { - try { - this.#handler = this.#parser ? this.#parser(value, this) : this.#defaultParser(value); - } catch (error) { - this.#handler = null; - } - } else { - this.#handler = value; - } - } - - /** - * Manually triggers the propagation with an optional event. - * If no event is provided and an event name is set, creates a new event. - * @param {Event} [event] - Optional event to propagate - */ - propagate(event?: Event): void { - if (!event && this.#eventName) { - event = new Event(this.#eventName); - } - if (!event) return; - this.#handleEvent(event); - } - - /** - * Cleans up the propagator by removing event listeners and clearing references. - * Should be called when the propagator is no longer needed. - */ - dispose(): void { - if (this.#source && this.#eventName) { - this.#source.removeEventListener(this.#eventName, this.#handleEvent); - } - this.#source = null; - this.#target = null; - this.#handler = null; - } - - #handleEvent = (event: Event) => { - if (!this.#source || !this.#target || !this.#handler) return; - - try { - this.#handler(this.#source, this.#target, event); - } catch (error) { - console.error('Error in propagator handler:', error); - } - }; - - // This approach turns object syntax into imperative code. - // We could alternatively use a parser (i.e. Babel) to statically analyse the code - // and check on keystrokes if the code is valid. - // We should try a few different things to figure out how this class could work best. - #defaultParser = (body: string): PropagatorFunction | null => { - const processedExp = body.trim(); - - const codeLines: string[] = []; - - // Split the expression into lines, handling different line endings - const lines = processedExp.split(/\r?\n/); - - for (const line of lines) { - let line_trimmed = line.trim(); - if (!line_trimmed) continue; - - // Remove trailing comma if it exists (only if it's at the very end of the line) - if (line_trimmed.endsWith(',')) { - line_trimmed = line_trimmed.slice(0, -1).trim(); - } - - // Find the first colon index, which separates the key and value. - // Colons can still be used in ternary operators or other expressions, - const colonIndex = line_trimmed.indexOf(':'); - if (colonIndex === -1) { - continue; - } - - const key = line_trimmed.slice(0, colonIndex).trim(); - const value = line_trimmed.slice(colonIndex + 1).trim(); - - if (key === '()') { - // Anonymous function: directly evaluate the value - codeLines.push(`${value};`); - } else if (key.endsWith('()')) { - // If the key is a method, execute it if the condition is true - const methodName = key.slice(0, -2); - codeLines.push(` - if (typeof to.${methodName} !== 'function') throw new Error(\`Method '${methodName}' does not exist on the target.\`); - else if (${value}) to.${methodName}();`); - } else { - // For property assignments, assign the value directly - codeLines.push(` - if (!('${key}' in to)) throw new Error(\`Property '${key}' does not exist on the target.\`); - to.${key} = ${value};`); - } - } - - const functionBody = codeLines.join('\n'); - - try { - const handler = new Function('from', 'to', 'event', functionBody) as PropagatorFunction; - this.#onParse?.(functionBody); - return handler; - } catch (error) { - this.#onError?.(error as Error); - return null; - } - }; -} diff --git a/lib/client-rect-observer.ts b/lib/client-rect-observer.ts index 6891408..9926950 100644 --- a/lib/client-rect-observer.ts +++ b/lib/client-rect-observer.ts @@ -21,7 +21,6 @@ export class ClientRectObserver { #rootRect = this.#root.getBoundingClientRect(); #entries: ClientRectObserverEntry[] = []; - #rafId = -1; #callback: ClientRectObserverCallback; diff --git a/lib/folk-element.ts b/lib/folk-element.ts index 2339a00..d6ae6b2 100644 --- a/lib/folk-element.ts +++ b/lib/folk-element.ts @@ -1,9 +1,20 @@ import { ReactiveElement } from '@lit/reactive-element'; -// will eventually extend Lit's ReactiveElement +/** + * Base class for all custom elements. Extends Lit's `ReactiveElement` and adds some utilities for defining the element. + * ```ts + * class MyElement extends FolkElement { + * static tagName = 'my-element'; + * } + * + * MyElement.define(); + * ``` + */ export class FolkElement extends ReactiveElement { + /** Defines the name of the custom element, must include a hyphen or it will error out when defined. */ static tagName = ''; + /** Defines the custom element with the global CustomElementRegistry, ignored if called more than once. Errors if no tagName is defined or it doesn't include a hyphen. */ static define() { if (customElements.get(this.tagName)) return; diff --git a/lib/iframe-script.ts b/lib/iframe-script.ts index e0385ac..fddbbad 100644 --- a/lib/iframe-script.ts +++ b/lib/iframe-script.ts @@ -1,3 +1,5 @@ +// this needs to move to the web extension once that's set up. + import { ClientRectObserverEntry } from './client-rect-observer.ts'; import { FolkObserver } from './folk-observer.ts'; diff --git a/lib/rAF.ts b/lib/rAF.ts index 2145899..4ffa061 100644 --- a/lib/rAF.ts +++ b/lib/rAF.ts @@ -1,3 +1,10 @@ +/** + * The nature of this project requires that we have multiple game/simulation loops running at the same time. + * This usually means multiple calls to window.requestAnimationFrame, which turns out is very costly. + * These helper functions batch calls to window.requestAnimationFrame to avoid that overhead. + * TODO: add some benchmarks for this +*/ + const callbacks = new Set(); let rAFId = -1; @@ -8,7 +15,7 @@ function onRAF(time: DOMHighResTimeStamp) { values.forEach((callback) => callback(time)); } -// Batch multiple callbacks into a single rAF for better performance +/** Batch calls to window.requestAnimationFrame */ export function requestAnimationFrame(callback: FrameRequestCallback) { if (callbacks.size === 0) { rAFId = window.requestAnimationFrame(onRAF); @@ -17,6 +24,7 @@ export function requestAnimationFrame(callback: FrameRequestCallback) { callbacks.add(callback); } +/** Batch calls to window.cancelAnimationFrame */ export function cancelAnimationFrame(callback: FrameRequestCallback) { callbacks.delete(callback); diff --git a/website/canvas/proximity-based-music.html b/website/canvas/proximity-based-music.html index 22c7ea9..47821bf 100644 --- a/website/canvas/proximity-based-music.html +++ b/website/canvas/proximity-based-music.html @@ -81,7 +81,7 @@ function updateFlowerProximity(flower) { const alreadyIntersection = proximitySet.has(flower); - // TODO: refactor this hack once resizing and the vertices API are figured out + const isNowIntersecting = aabbIntersection( recordPlayerGeometry.getTransformDOMRect(), flower.getTransformDOMRect(),