diff --git a/src/draggable.attempts/2/index.html b/src/draggable.attempts/2/index.html new file mode 100644 index 0000000..7961859 --- /dev/null +++ b/src/draggable.attempts/2/index.html @@ -0,0 +1,57 @@ + + + + + + Document + + + +
+
+
+
+ + + diff --git a/src/draggable.attempts/2/mod.ts b/src/draggable.attempts/2/mod.ts new file mode 100644 index 0000000..6f841d1 --- /dev/null +++ b/src/draggable.attempts/2/mod.ts @@ -0,0 +1,292 @@ +const rectangle = document.getElementById("rectangle")!; +const handle = document.getElementById("handle")!; +const reference = document.getElementById("reference")!; + +class Vec2 { + constructor(public x: number, public y: number) {} + + toString() { + return `Vec<${this.x.toString().padStart(3, " ")}, ${this.y + .toString() + .padStart(3, " ")}>`; + } + + clone() { + return new Vec2(this.x, this.y); + } + + eq(v: Vec2) { + return this.x === v.x && this.y === v.y; + } + + add(x: number, y: number): Vec2; + add(v: Vec2): Vec2; + add(c: number): Vec2; + add(x: number | Vec2, y?: number) { + if (x instanceof Vec2) return new Vec2(this.x + x.x, this.y + x.y); + if (typeof y === "number") return new Vec2(this.x + x, this.y + y); + return new Vec2(this.x + x, this.y + x); + } + + sub(x: number, y: number): Vec2; + sub(v: Vec2): Vec2; + sub(c: number): Vec2; + sub(x: number | Vec2, y?: number) { + if (x instanceof Vec2) return new Vec2(this.x - x.x, this.y - x.y); + if (typeof y === "number") return new Vec2(this.x - x, this.y - y); + return new Vec2(this.x - x, this.y - x); + } + + mult(x: number, y: number): Vec2; + mult(v: Vec2): Vec2; + mult(c: number): Vec2; + mult(x: number | Vec2, y?: number) { + if (x instanceof Vec2) return new Vec2(this.x * x.x, this.y * x.y); + if (typeof y === "number") return new Vec2(this.x * x, this.y * y); + return new Vec2(this.x * x, this.y * x); + } + + div(x: number, y: number): Vec2; + div(v: Vec2): Vec2; + div(c: number): Vec2; + div(x: number | Vec2, y?: number) { + if (x instanceof Vec2) return new Vec2(this.x / x.x, this.y / x.y); + if (typeof y === "number") return new Vec2(this.x / x, this.y / y); + return new Vec2(this.x / x, this.y / x); + } +} + +class State { + public dragging: boolean; + public origin: Vec2; + public pos: Vec2; + public size: Vec2; + public rot: number; + public cursor: Vec2; + + constructor({ + dragging, + origin, + pos, + size, + rot, + cursor, + }: { + dragging: boolean; + origin: Vec2; + pos: Vec2; + size: Vec2; + rot: number; + cursor: Vec2; + }) { + this.dragging = dragging; + this.origin = origin; + this.pos = pos; + this.size = size; + this.rot = rot; + this.cursor = cursor; + } + + toString() { + return ( + `State [\n` + + ` dragging: ${this.dragging},\n` + + ` origin: ${this.origin},\n` + + ` pos: ${this.pos},\n` + + ` rot: ${this.rot},\n` + + ` cursor: ${this.cursor}\n` + + `]` + ); + } + + clone() { + return new State({ + dragging: this.dragging, + origin: this.origin.clone(), + size: this.size.clone(), + pos: this.pos.clone(), + rot: this.rot, + cursor: this.cursor?.clone(), + }); + } + + eq(s: State) { + return ( + this.dragging === s.dragging && + this.origin.eq(s.origin) && + this.pos.eq(s.pos) && + this.rot === s.rot && + this.cursor.eq(s.cursor) + ); + } +} + +function getCursorPositionRelativeToElement( + cursor: Vec2, + size: Vec2, + element: HTMLElement, +) { + const boundingRect = element.getBoundingClientRect(); + + const computedStyle = window.getComputedStyle(element); + const transformValue = computedStyle.transform; + + if (transformValue === "none" || !transformValue) + return cursor.sub(boundingRect.left, boundingRect.top); + + const matrix = new DOMMatrix(transformValue); + + const centerX = boundingRect.left + boundingRect.width / 2; + const centerY = boundingRect.top + boundingRect.height / 2; + + // temporarily convert relative to center for easier calculations + const relativeToCenter = { + x: cursor.x - centerX, + y: cursor.y - centerY, + }; + + const inverseMatrix = matrix.inverse(); + + inverseMatrix.e = 0; + inverseMatrix.f = 0; + + const transformedPoint = { + x: + relativeToCenter.x * inverseMatrix.a + + relativeToCenter.y * inverseMatrix.c, + y: + relativeToCenter.x * inverseMatrix.b + + relativeToCenter.y * inverseMatrix.d, + }; + + // restore relative to top-left + return new Vec2( + transformedPoint.x + size.x / 2, + transformedPoint.y + size.y / 2, + ); +} + +interface Transform { + translation: Vec2; // equivalent to state.pos + rotation: number; // equivalent to state.rot + scale?: Vec2; // if you need scaling, defaults to (1,1) + origin: Vec2; // equivalent to state.origin +} + +function getCursorPositionRelativeToElement2( + cursor: Vec2, // cursor in page coordinates + size: Vec2, // original element size + transform: Transform, +) { + const scale = transform.scale ?? new Vec2(1, 1); + + // First get cursor position relative to the element's translated position + const relativeToElement = cursor.sub(transform.translation); + + // Calculate the actual pivot point (origin) for transformations + const pivotPoint = transform.origin; + + // Get position relative to pivot point + const relativeToPivot = relativeToElement.sub(pivotPoint); + + // Apply inverse rotation around pivot + const cosTheta = Math.cos(-transform.rotation); + const sinTheta = Math.sin(-transform.rotation); + const rotatedPoint = new Vec2( + relativeToPivot.x * cosTheta - relativeToPivot.y * sinTheta, + relativeToPivot.x * sinTheta + relativeToPivot.y * cosTheta, + ); + + // Apply inverse scale if present + const scaledPoint = transform.scale ? rotatedPoint.div(scale) : rotatedPoint; + + // Add back pivot offset to get final position + return scaledPoint.add(pivotPoint); +} + +const rect = rectangle.getBoundingClientRect(); + +// state is the source of truth +const state = new State({ + dragging: false, + + // initial origin + origin: new Vec2(rect.width / 2, rect.height / 2), + + // initial position of the rectangle + pos: new Vec2(0, 0), + + // size of the rectangle + size: new Vec2(rect.width, rect.height), + + // initial rotation + rot: 0, + + // placeholder cursor position + cursor: new Vec2(0, 0), +}); + +{ + rectangle.style.transformOrigin = `${state.origin.x}px ${state.origin.y}px`; + rectangle.style.transform = `translate(${state.pos.x}px, ${state.pos.y}px) rotate(${state.rot}rad)`; + handle.style.transform = `translate(${state.origin.x}px, ${state.origin.y}px)`; +} + +let prev = state.clone(); + +{ + const rect = rectangle.getBoundingClientRect(); + reference.style.transformOrigin = `${state.origin.x}px ${state.origin.y}px`; + reference.style.transform = `translate(${rect.left}px, ${rect.top}px)`; + reference.style.width = `${rect.width}px`; + reference.style.height = `${rect.height}px`; +} + +rectangle.addEventListener("mousedown", e => { + prev = state.clone(); + state.dragging = true; +}); + +window.addEventListener("mouseup", () => { + state.dragging = false; +}); + +const degree = 180 / Math.PI; + +window.addEventListener("mousemove", e => { + state.cursor = new Vec2(e.pageX, e.pageY); + + if (!state.dragging) return; + + const deltaCursor = state.cursor.sub(prev.cursor); + + state.pos = state.pos.add(deltaCursor); + + const rect = rectangle.getBoundingClientRect(); + reference.style.transformOrigin = `${state.origin.x}px ${state.origin.y}px`; + reference.style.transform = `translate(${rect.left}px, ${rect.top}px)`; + reference.style.width = `${rect.width}px`; + reference.style.height = `${rect.height}px`; + + // state.origin = state.cursor.sub(new Vec2(rect.left, rect.top)); + // state.rot = 0.05 + prev.rot; + state.origin = getCursorPositionRelativeToElement2( + state.cursor, + state.size, + // rectangle, + { + translation: state.pos, + rotation: state.rot, + origin: state.origin, + }, + ); + + if (!state.eq(prev)) { + // always keep DOM updated to state + rectangle.style.transformOrigin = `${state.origin.x}px ${state.origin.y}px`; + rectangle.style.transform = `translate(${state.pos.x}px, ${state.pos.y}px) rotate(${state.rot}rad)`; + handle.style.transform = `translate(${state.origin.x}px, ${state.origin.y}px)`; + } + + prev = state.clone(); +}); diff --git a/src/draggable.attempts/2/vite.config.ts b/src/draggable.attempts/2/vite.config.ts new file mode 100644 index 0000000..75299c4 --- /dev/null +++ b/src/draggable.attempts/2/vite.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from "vite"; +import { resolve } from "path"; + +// https://vitejs.dev/config/ +export default defineConfig({ + server: { port: 10000, allowedHosts: ["dev.mkr.thefeathers.co"] }, + plugins: [], + build: { + rollupOptions: { + input: { + main: resolve(__dirname, "index.html"), + }, + }, + }, +});