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.
This commit is contained in:
Lentil Hoffman 2026-07-01 14:15:43 -05:00
parent de46a13dfb
commit 7a9fb5ac0c
Signed by: lentil
GPG Key ID: 0F5B99F3F4D0C087
8 changed files with 289 additions and 251 deletions

View File

@ -12,68 +12,10 @@
</body>
</html>
<script src="./js/util.js"></script>
<!-- <script src="./js/util.js"></script>
<script src="./js/element.js"></script>
<script src="./js/elements.js"></script>
<script src="./js/pointer.js"></script>
<script src="./js/canvas.js"></script>
<script src="./js/main.js"></script>
<script defer>
// 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
// Add utility elements
addElement({
id: "time",
classes: ["monospace"],
detail: () => {
const t = 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}`;
},
});
addElement({
id: "pointer-info",
classes: ["monospace"],
detail: () => (["x", "y"]
.map(q => `${q}:${(pointer[q] ?? 0).toFixed(2).padStart(7, " ")}`)
.join(", ")
.replace(/ /g, "&nbsp;")),
});
addElement({
classes: ["monospace"],
id: 'pointer-history-length',
detail: () => `ptr hist len: ${pointerHistory.length}`,
});
addElement({
summary: () => "Element Summary",
detail: () => "Element Detail<br> With multile lines<br> How about that?",
});
// Run main loop
main();
</script>
<script src="./js/main.js"></script> -->
<script defer type="module" src="./js/index.js"></script>

View File

@ -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);
}

57
js/canvases.js Normal file
View File

@ -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);
}
}

View File

@ -1,4 +1,4 @@
class Element {
export class Element {
elements = undefined;
id = undefined;
summary = () => "";
@ -6,7 +6,11 @@ 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;
@ -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");
@ -39,12 +43,12 @@ class Element {
// 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`;

View File

@ -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,7 +33,8 @@ 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
@ -46,7 +51,7 @@ class Elements {
}
getElementFromDOM(target) {
return Array.from(this.elements.values()).find(({el}) => el === target);
return Array.from(this.elements.values()).find(({ el }) => el === target);
}
renderAll() {

58
js/index.js Normal file
View File

@ -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, "&nbsp;")),
});
main.addElement({
classes: ["monospace"],
id: 'pointer-history-length',
detail: () => `ptr hist len: ${main.pointer.history.length}`,
});
main.addElement({
summary: () => "Element Summary",
detail: () => "Element Detail<br> With multile lines<br> How about that?",
});
// Run main loop
main.start();

View File

@ -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);
}
}

View File

@ -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,
});
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");
});
}
// 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
// }
//})
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();
}
}
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();
}
}
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();
}
}