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.
111 lines
3.9 KiB
JavaScript
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();
|
|
}
|
|
}
|
|
|
|
} |