From 7a9fb5ac0ced842f8fa093aac746a4e7a705d225 Mon Sep 17 00:00:00 2001 From: Lentil Hoffman Date: Wed, 1 Jul 2026 14:15:43 -0500 Subject: [PATCH] Refactor to classes This pattern is a natural fit for the js module system in the browser. This gets rid of globals. Instead, index.js creates an instance of Main, which then instantiates each of the specialized classes. We should probably do class inheritance for the setMain pattern. On the other hand it's currently only 3 lines of code, so we'd only be saving 1 LOC per class, while creating a new class, and it wouldn't really be simpler. If our classes take on more features, we can further refactor. --- index.html | 64 +--------------- js/canvas.js | 53 ------------- js/canvases.js | 57 ++++++++++++++ js/element.js | 26 ++++--- js/elements.js | 25 ++++--- js/index.js | 58 +++++++++++++++ js/main.js | 60 +++++++++------ js/pointer.js | 197 +++++++++++++++++++++++++------------------------ 8 files changed, 289 insertions(+), 251 deletions(-) delete mode 100644 js/canvas.js create mode 100644 js/canvases.js create mode 100644 js/index.js diff --git a/index.html b/index.html index e1ebb70..60d4e36 100644 --- a/index.html +++ b/index.html @@ -12,68 +12,10 @@ - + + \ No newline at end of file diff --git a/js/canvas.js b/js/canvas.js deleted file mode 100644 index f364f37..0000000 --- a/js/canvas.js +++ /dev/null @@ -1,53 +0,0 @@ -// Background canvas - -let bgCanvas; -let fgCanvas; -let bgCtx; -let fgCtx; - -function initializeCanvas() { - initializeBackground(); - initializeForeground(); - fullscreen(); - window.addEventListener('resize', fullscreen); -} - -function initializeBackground() { - bgCanvas = document.createElement("canvas"); - mainDiv.appendChild(bgCanvas); - bgCanvas.classList.add("fullscreen"); - bgCanvas.classList.add("background"); - bgCtx = bgCanvas.getContext("2d"); - clearCanvas(bgCanvas, bgCtx); -} - -function initializeForeground() { - fgCanvas = document.createElement("canvas"); - mainDiv.appendChild(fgCanvas); - fgCanvas.classList.add("fullscreen"); - fgCanvas.classList.add("foreground"); - fgCtx = fgCanvas.getContext("2d"); - clearCanvas(fgCanvas, fgCtx); -} - -function clearCanvas(canvas, ctx) { - if (!canvas && !ctx) { - clearCanvas(bgCanvas, bgCtx); - clearCanvas(fgCanvas, fgCtx); - return; - } - ctx.clearRect(0, 0, canvas.width, canvas.height); - if (ctx === bgCtx) { - bgCtx.fillStyle = '#ccc'; - bgCtx.fillRect(0, 0, bgCanvas.width, bgCanvas.height); - } -} - -function fullscreen() { - const dim = { - width: document.documentElement.clientWidth, - height: document.documentElement.clientHeight, - }; - Object.assign(bgCanvas, dim); - Object.assign(fgCanvas, dim); -} diff --git a/js/canvases.js b/js/canvases.js new file mode 100644 index 0000000..22ab689 --- /dev/null +++ b/js/canvases.js @@ -0,0 +1,57 @@ +export class Canvases { + bgCanvas; + fgCanvas; + bgCtx; + fgCtx; + + setMain(main) { + this.main = main; + } + + initialize() { + this.initializeBackground(); + this.initializeForeground(); + this.fullscreen(); + window.addEventListener('resize', () => this.fullscreen()); + } + + initializeBackground() { + this.bgCanvas = document.createElement("canvas"); + this.main.div.appendChild(this.bgCanvas); + this.bgCanvas.classList.add("fullscreen"); + this.bgCanvas.classList.add("background"); + this.bgCtx = this.bgCanvas.getContext("2d"); + this.clear(this.bgCanvas, this.bgCtx); + } + + initializeForeground() { + this.fgCanvas = document.createElement("canvas"); + this.main.div.appendChild(this.fgCanvas); + this.fgCanvas.classList.add("fullscreen"); + this.fgCanvas.classList.add("foreground"); + this.fgCtx = this.fgCanvas.getContext("2d"); + this.clear(this.fgCanvas, this.fgCtx); + } + + clear(canvas, ctx) { + if (!canvas && !ctx) { + this.clear(this.bgCanvas, this.bgCtx); + this.clear(this.fgCanvas, this.fgCtx); + return; + } + ctx.clearRect(0, 0, canvas.width, canvas.height); + if (ctx === this.bgCtx) { + this.bgCtx.fillStyle = '#ccc'; + this.bgCtx.fillRect(0, 0, this.bgCanvas.width, this.bgCanvas.height); + } + } + + fullscreen() { + const dim = { + width: document.documentElement.clientWidth, + height: document.documentElement.clientHeight, + }; + Object.assign(this.bgCanvas, dim); + Object.assign(this.fgCanvas, dim); + } +} \ No newline at end of file diff --git a/js/element.js b/js/element.js index 4c4a26e..e09c1f1 100644 --- a/js/element.js +++ b/js/element.js @@ -1,4 +1,4 @@ -class Element { +export class Element { elements = undefined; id = undefined; summary = () => ""; @@ -6,12 +6,16 @@ class Element { el = undefined; classes = []; - constructor(elements, {id, summary, detail, classes, width, height, x, y}) { + setMain(main) { + this.main = main; + } + + constructor(elements, { id, summary, detail, classes, width, height, x, y }) { this.elements = elements; this.id = id; this.summary = summary; this.detail = detail; - + // Create DOM element(s) this.el = document.createElement("div"); this.el.id = id; @@ -23,7 +27,7 @@ class Element { this.setSize(width, height); } if (x !== undefined && y !== undefined) { - this.setPosition({x, y}); + this.setPosition({ x, y }); } if (summary) { this.summaryEl = document.createElement("div"); @@ -35,28 +39,28 @@ class Element { this.detailEl.classList.add("detail"); this.el.appendChild(this.detailEl) } - + // Handle pointer down to initiate drag this.el.addEventListener("pointerdown", (e) => { e.preventDefault(); - const {clientX: x, clientY: y} = e; - startDrag({element: this, x, y}); + const { clientX: x, clientY: y } = e; + this.main.pointer.startDrag({ element: this, x, y }); }); } - - setPosition({x, y}) { + + setPosition({ x, y }) { this.el.style.position = "absolute"; this.el.style.top = `${y}px`; this.el.style.left = `${x}px`; return this; } - + setSize(width, height) { this.el.style.width = `${width}px`; this.el.style.height = `${height}px`; return this; } - + render() { if (this.summary) { this.summaryEl.innerHTML = this.summary(); diff --git a/js/elements.js b/js/elements.js index e0873f4..c88c205 100644 --- a/js/elements.js +++ b/js/elements.js @@ -1,10 +1,11 @@ -// globals: mainDiv, crypto -class Elements { +import { Element } from "./element.js"; + +export class Elements { elements = new Map(); // id -> Element classes = []; div = undefined; - - constructor({classes} = {}) { + + constructor({ classes } = {}) { this.classes = classes; this.div = document.createElement("div"); this.div.classList.add("elements"); @@ -12,7 +13,10 @@ class Elements { for (const className of (classes ?? [])) { this.div.classList.add(className); } - mainDiv.appendChild(this.div); + } + setMain(main) { + this.main = main; + this.main.div.appendChild(this.div); } add(props) { @@ -29,14 +33,15 @@ class Elements { } } // Create element - const element = new Element(this, {...props, id}); + const element = new Element(this, { ...props, id }); + element.setMain(this.main); // Append to div this.div.appendChild(element.el); // Add to collection this.elements.set(id, element); return element; } - + remove(id) { const element = this.elements.get(id); if (element) { @@ -44,11 +49,11 @@ class Elements { this.elements.delete(id); } } - + getElementFromDOM(target) { - return Array.from(this.elements.values()).find(({el}) => el === target); + return Array.from(this.elements.values()).find(({ el }) => el === target); } - + renderAll() { for (const element of this.elements.values()) { element.render(); diff --git a/js/index.js b/js/index.js new file mode 100644 index 0000000..c76be38 --- /dev/null +++ b/js/index.js @@ -0,0 +1,58 @@ +// Time-stepped to support animations; +// Can also run tasks queued by the UI, if we want; +// Could also support arbitrary scheduled operations. + +// Let's create an ontology for the UI, which the render loop can reference +// What kinds of things we have? +// Text : Consists of some text content; could have formatting; could have title; +// May want to specialize it to support data or whatever... +// Connection : Like a line / arrow ; May have different kinds of connections such as +// Precedence / hierarchy tree; depencency graph; +// May have multiple start / end points (a.k.a. may be a hyperedge) +// Also worth thinking about matrices - outer products +// Might as well further consider tensor products + +// So then maybe we have an output / display node; this could be displaying things such as +// Values of some parameters +// Graph of (historical trends of) parameters +// Results of logic computations / expression evaluation + +// Utility elements to start, for displaying document time and pointer location + +import { Main } from "./main.js"; +const main = new Main("mydeate-main"); + +// Add utility elements +main.addElement({ + id: "time", + classes: ["monospace"], + detail: () => { + const t = main.currentTime; + const s = t / 1000; + const m = s / 60; + const h = m / 60; + const d = h / 24; + const timeStr = [d % 60, h % 24, m % 60, s % 60].map(x => x.toFixed(0).padStart(2, "0")).join(":"); + return `runtime: ${timeStr}`; + }, +}); +main.addElement({ + id: "pointer-info", + classes: ["monospace"], + detail: () => (["x", "y"] + .map(q => `${q}:${(main.pointer[q] ?? 0).toFixed(2).padStart(7, " ")}`) + .join(", ") + .replace(/ /g, " ")), +}); +main.addElement({ + classes: ["monospace"], + id: 'pointer-history-length', + detail: () => `ptr hist len: ${main.pointer.history.length}`, +}); +main.addElement({ + summary: () => "Element Summary", + detail: () => "Element Detail
With multile lines
How about that?", +}); + +// Run main loop +main.start(); \ No newline at end of file diff --git a/js/main.js b/js/main.js index 6ce3c82..570ae9a 100644 --- a/js/main.js +++ b/js/main.js @@ -1,26 +1,44 @@ -const mainDiv = document.getElementById("mydeate-main"); -const elements = new Elements(); -// Initialize variables for main loop -let run = true; -let currentTime; +import { Elements } from "./elements.js"; +import { Pointer } from "./pointer.js"; +import { Canvases } from "./canvases.js"; -function main() { - initializeCanvas(); - initializePointer(); - requestAnimationFrame(loop); -} +const pointer = new Pointer(); -function loop(elapsedTime) { - if (run) { - currentTime = document.timeline.currentTime; - clearCanvas(); - updatePointerHistory({decay: true}); - drawPointerHistory(); - elements.renderAll(); +export class Main { + div = undefined; // Constructor finds this by id + // Child objects we create + canvases = new Canvases(); + elements = new Elements(); + pointer = new Pointer(); + // Initialize variables for main loop + run = true; + currentTime; + + constructor(mainDivId) { + this.div = document.getElementById(mainDivId); + this.canvases.setMain(this); + this.elements.setMain(this); + this.pointer.setMain(this); } - requestAnimationFrame(loop); -} -function addElement(params) { - return elements.add(params); + start() { + this.canvases.initialize(); + this.pointer.initialize(); + requestAnimationFrame(() => this.loop()); + } + + loop(elapsedTime) { + if (this.run) { + this.currentTime = document.timeline.currentTime; + this.canvases.clear(); + this.pointer.updateHistory({ decay: true }); + this.pointer.drawHistory(); + this.elements.renderAll(); + } + requestAnimationFrame(() => this.loop()); + } + + addElement(params) { + return this.elements.add(params); + } } \ No newline at end of file diff --git a/js/pointer.js b/js/pointer.js index c873eed..c14444e 100644 --- a/js/pointer.js +++ b/js/pointer.js @@ -1,104 +1,111 @@ -// Let's set up pointer tracking. We can use the main div as the pointer target. -// Note that we may later add pointer handlers on layered elements -const pointer = {x: undefined, y: undefined}; -// Trace the path of the cursor -const pointerHistory = []; -const maxHistory = 50; -const dropTargetMaxRange = 20; -const drag = { - start: {x: undefined, y: undefined, t: undefined}, - element: undefined, - placeholder: undefined, -} +export class Pointer { + setMain(main) { + this.main = main; + } -function startDrag({element, x, y}) { - drag.element = element; - drag.start = {x, y, t: currentTime}; - // We use a placeholder to represent the new position - const {x: ox, y: oy, width, height} = element.el.getBoundingClientRect(); - drag.placeholder = elements.add({ - ...element, - id: `${this.id}-placeholder`, - classes: [...Array.from(element.el.classList.values()), "moving", "placeholder"], - width, height, x, y, - }); - drag.element.el.classList.add("moving"); - mainDiv.classList.add("dragging"); -} + // Let's set up pointer tracking. We can use the main div as the pointer target. + // Note that we may later add pointer handlers on layered elements + position = { x: undefined, y: undefined }; + // Trace the path of the cursor + history = []; + maxHistory = 50; + dropTargetMaxRange = 20; + drag = { + start: { x: undefined, y: undefined, t: undefined }, + element: undefined, + placeholder: undefined, + } -function initializePointer() { - mainDiv.addEventListener("pointermove", (e) => { - const {clientX: x, clientY: y} = e; - Object.assign(pointer, {x, y}); + startDrag({ element, x, y }) { + this.drag.element = element; + this.drag.start = { x, y, t: this.main.currentTime }; + // We use a placeholder to represent the new position + const { x: ox, y: oy, width, height } = element.el.getBoundingClientRect(); + this.drag.placeholder = this.main.elements.add({ + ...element, + id: `${this.id}-placeholder`, + classes: [...Array.from(element.el.classList.values()), "moving", "placeholder"], + width, height, x, y, + }); + this.drag.element.el.classList.add("moving"); + this.main.div.classList.add("dragging"); + } - if (drag.start.t) { - const displacement = { - x: x - drag.start.x, - y: y - drag.start.y, + initialize() { + this.main.div.addEventListener("pointermove", (e) => { + const { clientX: x, clientY: y } = e; + Object.assign(this.position, { x, y }); + + if (this.drag.start.t) { + const displacement = { + x: x - this.drag.start.x, + y: y - this.drag.start.y, + }; + const { x: ox, y: oy } = this.drag.element.el.getBoundingClientRect(); + this.drag.placeholder.setPosition({ + x: ox + displacement.x, + y: oy + displacement.y, + }); + + // We can check here if we're near one or more drop targets to offer. + // We should indicate which available drop target is currently active. + // If there are multiple options, we can show them. + // const placeholder; + // const nearbyTargets = Array.from(elements.values()).filter(({el}) => { + // const {x, y, width, height} = el.getBoundingClientRect(); + // const linearDist = minLinearDist(el.getBoundingClientRect(), ); + // if (linearDist <= dropTargetMaxRange) { + // Visually activate the drop target + // } + //}) + } + }); + + this.main.div.addEventListener("pointerup", (e) => { + if (!this.drag.start.t) return; + const { clientX: x, clientY: y } = e; + // Displacement + const d = { + x: x - this.drag.start.x, + y: y - this.drag.start.y, }; - const {x: ox, y: oy} = drag.element.el.getBoundingClientRect(); - drag.placeholder.setPosition({ - x: ox + displacement.x, - y: oy + displacement.y, - }); - - // We can check here if we're near one or more drop targets to offer. - // We should indicate which available drop target is currently active. - // If there are multiple options, we can show them. - // const placeholder; - // const nearbyTargets = Array.from(elements.values()).filter(({el}) => { - // const {x, y, width, height} = el.getBoundingClientRect(); - // const linearDist = minLinearDist(el.getBoundingClientRect(), ); - // if (linearDist <= dropTargetMaxRange) { - // Visually activate the drop target - // } - //}) + const { x: ox, y: oy, width, height } = this.drag.element.el.getBoundingClientRect(); + this.drag.element.setPosition({ x: ox + d.x, y: oy + d.y }).setSize(width, height); + this.drag.element.el.classList.remove("moving"); + this.main.elements.remove(this.drag.placeholder.id); + this.drag.start = {}; + this.drag.placeholder = undefined; + this.drag.element = undefined; + this.main.div.classList.remove("dragging"); + }); + } + + updateHistory({ decay } = { decay: true }) { + if (this.position.x === undefined || this.position.y === undefined) return; + if (!this.history.length) { + this.history.push({ ...this.position, t: this.main.currentTime }); + return; + } + const lastPointer = this.history[this.history.length - 1]; + if (decay || this.position.x !== lastPointer.x || this.position.y !== lastPointer.y) { + this.history.push({ ...this.position, t: this.main.currentTime }); + } + while (this.history.length > this.maxHistory) { + this.history.shift(); } - }); - - mainDiv.addEventListener("pointerup", (e) => { - if (!drag.start.t) return; - const {clientX: x, clientY: y} = e; - // Displacement - const d = { - x: x - drag.start.x, - y: y - drag.start.y, - }; - const {x: ox, y: oy, width, height} = drag.element.el.getBoundingClientRect(); - drag.element.setPosition({ x: ox + d.x, y: oy + d.y}).setSize(width, height); - drag.element.el.classList.remove("moving"); - elements.remove(drag.placeholder.id); - drag.start = {}; - drag.placeholder = undefined; - drag.element = undefined; - mainDiv.classList.remove("dragging"); - }); -} - -function updatePointerHistory({decay} = {decay: true}) { - if (pointer.x === undefined || pointer.y === undefined) return; - if (!pointerHistory.length) { - pointerHistory.push({...pointer, t: currentTime}); - return; } - const lastPointer = pointerHistory[pointerHistory.length - 1]; - if (decay || pointer.x !== lastPointer.x || pointer.y !== lastPointer.y) { - pointerHistory.push({...pointer, t: currentTime}); - } - while (pointerHistory.length > maxHistory) { - pointerHistory.shift(); - } -} -function drawPointerHistory() { - if (pointerHistory.length < 2) return; - for (let i = 1; i < pointerHistory.length; i++) { - fgCtx.beginPath(); - const opacity = i / pointerHistory.length; - fgCtx.strokeStyle = `rgba(128, 0, 0, ${opacity})`; - fgCtx.moveTo(pointerHistory[i - 1].x, pointerHistory[i - 1].y); - fgCtx.lineTo(pointerHistory[i].x, pointerHistory[i].y); - fgCtx.stroke(); + drawHistory() { + const { fgCtx } = this.main.canvases; + if (this.history.length < 2) return; + for (let i = 1; i < this.history.length; i++) { + fgCtx.beginPath(); + const opacity = i / this.history.length; + fgCtx.strokeStyle = `rgba(128, 0, 0, ${opacity})`; + fgCtx.moveTo(this.history[i - 1].x, this.history[i - 1].y); + fgCtx.lineTo(this.history[i].x, this.history[i].y); + fgCtx.stroke(); + } } -} +} \ No newline at end of file