mydeate/js/pointer.js
Lentil Hoffman 7a9fb5ac0c
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.
2026-07-01 14:15:43 -05:00

111 lines
3.9 KiB
JavaScript

export class Pointer {
setMain(main) {
this.main = main;
}
// 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,
}
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");
}
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, 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();
}
}
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();
}
}
}