Browse Source

feat: attempt 2 at creating standalone draaggable

master
Muthu Kumar 1 month ago
parent
commit
32c7e4a5d1
Failed to extract signature
  1. 57
      src/draggable.attempts/2/index.html
  2. 292
      src/draggable.attempts/2/mod.ts
  3. 15
      src/draggable.attempts/2/vite.config.ts

57
src/draggable.attempts/2/index.html

@ -0,0 +1,57 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<style>
body {
width: 100vw;
height: 100vh;
margin: 40px;
padding: 0;
}
#rectangle {
width: 200px;
height: 120px;
background-color: #3498db;
cursor: grab;
user-select: none;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
position: relative;
}
#handle {
width: 10px;
height: 10px;
background-color: #e74c3c;
border-radius: 50%;
position: absolute;
top: 0;
left: 0;
margin-left: -5px;
margin-top: -5px;
pointer-events: none;
}
#reference {
width: 200px;
height: 120px;
position: absolute;
top: 0;
left: 0;
opacity: 0.2;
background-color: #2ecc71;
pointer-events: none;
}
</style>
<div id="rectangle">
<div id="handle"></div>
</div>
<div id="reference"></div>
<script type="module" src="mod.ts"></script>
</body>
</html>

292
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();
});

15
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"),
},
},
},
});
Loading…
Cancel
Save